diff --git a/README.md b/README.md index 1725887..80f9268 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ Progress: - [ ] Actually make this an installable package with nice docs - [ ] Stretch goal: elm-review rule to set everything up! - ### Vendored packages This package vendors two other Elm packages in order to make modifications: @@ -31,8 +30,6 @@ This package vendors two other Elm packages in order to make modifications: Ideally these will be de-vendored into a regular Elm dependencies in future. - - ### Install Until this is available as a package: @@ -40,6 +37,7 @@ Until this is available as a package: - Clone this repo into your project as a git submodule (or vendor it manually by copy pasting src) - Reference `src` in your project's `elm.json:source-directories` - Install the relevant deps: + ``` yes | lamdera install elm/browser yes | lamdera install elm/bytes @@ -59,12 +57,10 @@ yes | lamdera install TSFoster/elm-sha1 You might also have luck with [elm-git-install](https://github.com/robinheghan/elm-git-install), though its not been tried yet. - ### Setup :warning: This is the conceptual target API, not actual instructions for this code yet! (Instead, follow the types!) - 1. Create `src/Auth.elm`: ```elm @@ -95,10 +91,9 @@ config = 2. Modify the 2 core Model types in `src/Types.elm`: - ```elm import Auth.Common -import Dict exposing (Dict) +import SeqDict as Dict exposing (Dict) import Lamdera import Url exposing (Url) diff --git a/elm.json b/elm.json index 41add34..7f207b6 100644 --- a/elm.json +++ b/elm.json @@ -26,8 +26,11 @@ "elm/time": "1.0.0 <= v < 2.0.0", "elm/url": "1.0.0 <= v < 2.0.0", "elmcraft/core-extra": "2.0.0 <= v < 3.0.0", + "ianmackenzie/elm-units": "2.10.0 <= v < 3.0.0", "ktonon/elm-crypto": "1.1.2 <= v < 2.0.0", - "ktonon/elm-word": "2.1.2 <= v < 3.0.0" + "ktonon/elm-word": "2.1.2 <= v < 3.0.0", + "lamdera/containers": "1.0.0 <= v < 2.0.0", + "lamdera/program-test": "3.0.0 <= v < 4.0.0" }, "test-dependencies": {} } diff --git a/src/Auth/Common.elm b/src/Auth/Common.elm index c5d14e1..12a2dea 100644 --- a/src/Auth/Common.elm +++ b/src/Auth/Common.elm @@ -1,67 +1,67 @@ module Auth.Common exposing (..) import Base64.Encode as Base64 -import Browser.Navigation exposing (Key) import Bytes exposing (Bytes) import Bytes.Encode as Bytes -import Dict exposing (Dict) -import Http -import Json.Decode as Json +import Duration +import Effect.Browser.Navigation +import Effect.Command exposing (BackendOnly, Command, FrontendOnly) +import Effect.Lamdera exposing (ClientId, SessionId) +import Effect.Process +import Effect.Task +import Effect.Time import OAuth import OAuth.AuthorizationCode as OAuth -import Process -import Task exposing (Task) -import Time import Url exposing (Protocol(..), Url) -type alias Config frontendMsg toBackend backendMsg toFrontend frontendModel backendModel = +type alias Config frontendMsg toBackend backendMsg toFrontend frontendModel backendModel toMsg = { toBackend : ToBackend -> toBackend , toFrontend : ToFrontend -> toFrontend , backendMsg : BackendMsg -> backendMsg - , sendToFrontend : SessionId -> toFrontend -> Cmd backendMsg - , sendToBackend : toBackend -> Cmd frontendMsg - , methods : List (Method frontendMsg backendMsg frontendModel backendModel) - , renewSession : SessionId -> ClientId -> backendModel -> ( backendModel, Cmd backendMsg ) + , sendToFrontend : SessionId -> toFrontend -> Command BackendOnly toFrontend backendMsg + , sendToBackend : toBackend -> Command FrontendOnly toBackend frontendMsg + , methods : List (Method frontendMsg backendMsg frontendModel backendModel FrontendOnly toMsg) + , renewSession : SessionId -> ClientId -> backendModel -> ( backendModel, Command BackendOnly toMsg backendMsg ) } -type Method frontendMsg backendMsg frontendModel backendModel - = ProtocolOAuth (ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel) - | ProtocolEmailMagicLink (ConfigurationEmailMagicLink frontendMsg backendMsg frontendModel backendModel) +type Method frontendMsg backendMsg frontendModel backendModel restriction toMsg + = ProtocolOAuth (ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel restriction toMsg) + | ProtocolEmailMagicLink (ConfigurationEmailMagicLink frontendMsg backendMsg frontendModel backendModel restriction toMsg) -type alias ConfigurationEmailMagicLink frontendMsg backendMsg frontendModel backendModel = +type alias ConfigurationEmailMagicLink frontendMsg backendMsg frontendModel backendModel restriction toMsg = { id : String , initiateSignin : SessionId -> ClientId -> backendModel -> { username : Maybe String } - -> Time.Posix - -> ( backendModel, Cmd backendMsg ) + -> Effect.Time.Posix + -> ( backendModel, Command BackendOnly toMsg backendMsg ) , onFrontendCallbackInit : frontendModel -> MethodId -> Url - -> Key - -> (ToBackend -> Cmd frontendMsg) - -> ( frontendModel, Cmd frontendMsg ) + -> Effect.Browser.Navigation.Key + -> (ToBackend -> Command restriction toMsg frontendMsg) + -> ( frontendModel, Command restriction toMsg frontendMsg ) , onAuthCallbackReceived : SessionId -> ClientId -> Url -> AuthCode -> State - -> Time.Posix + -> Effect.Time.Posix -> (BackendMsg -> backendMsg) -> backendModel - -> ( backendModel, Cmd backendMsg ) + -> ( backendModel, Command BackendOnly toMsg backendMsg ) , placeholder : frontendMsg -> backendMsg -> frontendModel -> backendModel -> () } -type alias ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel = +type alias ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel restriction toMsg = { id : String , authorizationEndpoint : Url , tokenEndpoint : Url @@ -70,14 +70,14 @@ type alias ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel , clientId : String , clientSecret : String , scope : List String - , getUserInfo : OAuth.AuthenticationSuccess -> Task Error UserInfo + , getUserInfo : OAuth.AuthenticationSuccess -> Effect.Task.Task restriction Error UserInfo , onFrontendCallbackInit : frontendModel -> MethodId -> Url - -> Key - -> (ToBackend -> Cmd frontendMsg) - -> ( frontendModel, Cmd frontendMsg ) + -> Effect.Browser.Navigation.Key + -> (ToBackend -> Command FrontendOnly toMsg frontendMsg) + -> ( frontendModel, Command FrontendOnly toMsg frontendMsg ) , placeholder : ( backendModel, backendMsg ) -> () } @@ -98,10 +98,10 @@ type ToBackend type BackendMsg - = AuthSigninInitiated_ { sessionId : SessionId, clientId : ClientId, methodId : MethodId, baseUrl : Url, now : Time.Posix, username : Maybe String } + = AuthSigninInitiated_ { sessionId : SessionId, clientId : ClientId, methodId : MethodId, baseUrl : Url, now : Effect.Time.Posix, username : Maybe String } | AuthSigninInitiatedDelayed_ SessionId ToFrontend - | AuthCallbackReceived_ SessionId ClientId MethodId Url String String Time.Posix - | AuthSuccess SessionId ClientId MethodId Time.Posix (Result Error ( UserInfo, Maybe Token )) + | AuthCallbackReceived_ SessionId ClientId MethodId Url String String Effect.Time.Posix + | AuthSuccess SessionId ClientId MethodId Effect.Time.Posix (Result Error ( UserInfo, Maybe Token )) | AuthRenewSession SessionId ClientId | AuthLogout SessionId ClientId @@ -122,8 +122,8 @@ type AuthChallengeReason type alias Token = { methodId : MethodId , token : OAuth.Token - , created : Time.Posix - , expires : Time.Posix + , created : Effect.Time.Posix + , expires : Effect.Time.Posix } @@ -179,14 +179,14 @@ type alias UserInfo = type alias PendingAuth = - { created : Time.Posix + { created : Effect.Time.Posix , sessionId : SessionId , state : String } type alias PendingEmailAuth = - { created : Time.Posix + { created : Effect.Time.Posix , sessionId : SessionId , username : String , fullname : String @@ -231,12 +231,12 @@ sleepTask isDev msg = -- make sure we sleep a little before a redirect otherwise we won't have our -- persisted state. (if isDev then - Process.sleep 3000 + Effect.Process.sleep (Duration.milliseconds 3000) else - Process.sleep 0 + Effect.Process.sleep (Duration.milliseconds 0) ) - |> Task.perform (always msg) + |> Effect.Task.perform (always msg) nothingIfEmpty s = @@ -253,11 +253,3 @@ nothingIfEmpty s = -- Lamdera aliases - - -type alias SessionId = - String - - -type alias ClientId = - String diff --git a/src/Auth/Flow.elm b/src/Auth/Flow.elm index 2c4fadc..4eb3a8d 100644 --- a/src/Auth/Flow.elm +++ b/src/Auth/Flow.elm @@ -2,18 +2,14 @@ module Auth.Flow exposing (..) import Auth.Common exposing (LogoutEndpointConfig(..), MethodId, ToBackend(..)) import Auth.Method.EmailMagicLink -import Auth.Method.OAuthGithub -import Auth.Method.OAuthGoogle import Auth.Protocol.OAuth -import Browser.Navigation as Navigation -import Dict exposing (Dict) +import Effect.Browser.Navigation as Navigation +import Effect.Command as Command exposing (BackendOnly, Command, FrontendOnly) +import Effect.Lamdera exposing (ClientId, SessionId, clientIdToString, sessionIdFromString) +import Effect.Task +import Effect.Time import List.Extra as List -import OAuth -import OAuth.AuthorizationCode as OAuth -import Process -import SHA1 -import Task -import Time +import SeqDict as Dict exposing (SeqDict) import Url exposing (Protocol(..), Url) import Url.Builder exposing (QueryParameter) @@ -23,8 +19,11 @@ init : -> Auth.Common.MethodId -> Url -> Navigation.Key - -> (Auth.Common.ToBackend -> Cmd frontendMsg) - -> ( { frontendModel | authFlow : Auth.Common.Flow, authRedirectBaseUrl : Url }, Cmd frontendMsg ) + -> (Auth.Common.ToBackend -> Command FrontendOnly toMsg frontendMsg) + -> + ( { frontendModel | authFlow : Auth.Common.Flow, authRedirectBaseUrl : Url } + , Command FrontendOnly toMsg frontendMsg + ) init model methodId origin navigationKey toBackendFn = case methodId of "EmailMagicLink" -> @@ -73,8 +72,8 @@ updateFromFrontend { asBackendMsg } clientId sessionId authToBackend model = Auth.Common.AuthCallbackReceived methodId receivedUrl code state -> ( model - , Time.now - |> Task.perform + , Effect.Time.now + |> Effect.Task.perform (\now -> asBackendMsg <| Auth.Common.AuthCallbackReceived_ @@ -90,9 +89,9 @@ updateFromFrontend { asBackendMsg } clientId sessionId authToBackend model = Auth.Common.AuthRenewSessionRequested -> ( model - , Time.now - |> Task.perform - (\t -> + , Effect.Time.now + |> Effect.Task.perform + (\_ -> asBackendMsg <| Auth.Common.AuthRenewSession sessionId clientId ) @@ -100,49 +99,63 @@ updateFromFrontend { asBackendMsg } clientId sessionId authToBackend model = Auth.Common.AuthLogoutRequested -> ( model - , Time.now - |> Task.perform - (\t -> + , Effect.Time.now + |> Effect.Task.perform + (\_ -> asBackendMsg <| Auth.Common.AuthLogout sessionId clientId ) ) -type alias BackendUpdateConfig frontendMsg backendMsg toFrontend frontendModel backendModel = +type alias BackendUpdateConfig frontendMsg backendMsg toFrontend frontendModel backendModel toMsg = { asToFrontend : Auth.Common.ToFrontend -> toFrontend , asBackendMsg : Auth.Common.BackendMsg -> backendMsg - , sendToFrontend : Auth.Common.SessionId -> toFrontend -> Cmd backendMsg - , backendModel : { backendModel | pendingAuths : Dict Auth.Common.SessionId Auth.Common.PendingAuth } - , loadMethod : Auth.Common.MethodId -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel) + , sendToFrontend : + SessionId + -> toFrontend + -> Command BackendOnly toMsg backendMsg + , backendModel : { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth } + , loadMethod : + Auth.Common.MethodId + -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel BackendOnly toMsg) , handleAuthSuccess : - Auth.Common.SessionId - -> Auth.Common.ClientId + SessionId + -> ClientId -> Auth.Common.UserInfo -> MethodId -> Maybe Auth.Common.Token - -> Time.Posix - -> ( { backendModel | pendingAuths : Dict Auth.Common.SessionId Auth.Common.PendingAuth }, Cmd backendMsg ) - , renewSession : Auth.Common.SessionId -> Auth.Common.ClientId -> backendModel -> ( backendModel, Cmd backendMsg ) - , logout : Auth.Common.SessionId -> Auth.Common.ClientId -> backendModel -> ( backendModel, Cmd backendMsg ) + -> Effect.Time.Posix + -> ( { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth }, Command BackendOnly toMsg backendMsg ) + , renewSession : SessionId -> ClientId -> backendModel -> ( backendModel, Command BackendOnly toMsg backendMsg ) + , logout : SessionId -> ClientId -> backendModel -> ( backendModel, Command BackendOnly toMsg backendMsg ) , isDev : Bool } backendUpdate : - BackendUpdateConfig - frontendMsg - backendMsg - toFrontend - frontendModel - { backendModel | pendingAuths : Dict Auth.Common.SessionId Auth.Common.PendingAuth } + BackendUpdateConfig frontendMsg backendMsg toFrontend frontendModel { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth } toMsg -> Auth.Common.BackendMsg - -> ( { backendModel | pendingAuths : Dict Auth.Common.SessionId Auth.Common.PendingAuth }, Cmd backendMsg ) + -> ( { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth }, Command BackendOnly toMsg backendMsg ) backendUpdate { asToFrontend, asBackendMsg, sendToFrontend, backendModel, loadMethod, handleAuthSuccess, renewSession, logout, isDev } authBackendMsg = let authError str = asToFrontend (Auth.Common.AuthError (Auth.Common.ErrAuthString str)) + withMethod : + Auth.Common.MethodId + -> SessionId + -> + (Auth.Common.Method frontendMsg backendMsg frontendModel { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth } BackendOnly toMsg + -> + ( { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth } + , Command BackendOnly toMsg backendMsg + ) + ) + -> + ( { backendModel | pendingAuths : SeqDict SessionId Auth.Common.PendingAuth } + , Command BackendOnly toMsg backendMsg + ) withMethod methodId clientId fn = case loadMethod methodId of Nothing -> @@ -156,7 +169,7 @@ backendUpdate { asToFrontend, asBackendMsg, sendToFrontend, backendModel, loadMe case authBackendMsg of Auth.Common.AuthSigninInitiated_ { sessionId, clientId, methodId, baseUrl, now, username } -> withMethod methodId - clientId + (clientId |> clientIdToString |> sessionIdFromString) (\method -> case method of Auth.Common.ProtocolEmailMagicLink config -> @@ -171,7 +184,7 @@ backendUpdate { asToFrontend, asBackendMsg, sendToFrontend, backendModel, loadMe Auth.Common.AuthCallbackReceived_ sessionId clientId methodId receivedUrl code state now -> withMethod methodId - clientId + (clientId |> clientIdToString |> sessionIdFromString) (\method -> case method of Auth.Common.ProtocolEmailMagicLink config -> @@ -187,8 +200,8 @@ backendUpdate { asToFrontend, asBackendMsg, sendToFrontend, backendModel, loadMe { backendModel_ | pendingAuths = backendModel_.pendingAuths |> Dict.remove sessionId } in withMethod methodId - clientId - (\method -> + (clientId |> clientIdToString |> sessionIdFromString) + (\_ -> case res of Ok ( userInfo, authToken ) -> handleAuthSuccess sessionId clientId userInfo methodId authToken now @@ -220,7 +233,10 @@ signOutRequested : Maybe LogoutEndpointConfig -> List QueryParameter -> { a | authFlow : Auth.Common.Flow, authLogoutReturnUrlBase : Url } - -> ( { a | authFlow : Auth.Common.Flow, authLogoutReturnUrlBase : Url }, Cmd msg ) + -> + ( { a | authFlow : Auth.Common.Flow, authLogoutReturnUrlBase : Url } + , Command FrontendOnly toMsg msg + ) signOutRequested maybeUrlConfig callBackQueries model = ( { model | authFlow = Auth.Common.Idle } , case maybeUrlConfig of @@ -247,7 +263,7 @@ signOutRequested maybeUrlConfig callBackQueries model = startProviderSignin : Url -> { frontendModel | authFlow : Auth.Common.Flow } - -> ( { frontendModel | authFlow : Auth.Common.Flow }, Cmd msg ) + -> ( { frontendModel | authFlow : Auth.Common.Flow }, Command FrontendOnly toMsg msg ) startProviderSignin url model = ( { model | authFlow = Auth.Common.Pending } , Navigation.load (Url.toString url) @@ -257,7 +273,7 @@ startProviderSignin url model = setError : { frontendModel | authFlow : Auth.Common.Flow } -> Auth.Common.Error - -> ( { frontendModel | authFlow : Auth.Common.Flow }, Cmd msg ) + -> ( { frontendModel | authFlow : Auth.Common.Flow }, Command restriction toMsg msg ) setError model err = setAuthFlow model <| Auth.Common.Errored err @@ -265,9 +281,9 @@ setError model err = setAuthFlow : { frontendModel | authFlow : Auth.Common.Flow } -> Auth.Common.Flow - -> ( { frontendModel | authFlow : Auth.Common.Flow }, Cmd msg ) + -> ( { frontendModel | authFlow : Auth.Common.Flow }, Command restriction toMsg msg ) setAuthFlow model flow = - ( { model | authFlow = flow }, Cmd.none ) + ( { model | authFlow = flow }, Command.none ) errorToString : Auth.Common.Error -> String @@ -276,10 +292,10 @@ errorToString error = Auth.Common.ErrStateMismatch -> "ErrStateMismatch" - Auth.Common.ErrAuthorization authorizationError -> + Auth.Common.ErrAuthorization _ -> "ErrAuthorization" - Auth.Common.ErrAuthentication authenticationError -> + Auth.Common.ErrAuthentication _ -> "ErrAuthentication" Auth.Common.ErrHTTPGetAccessToken -> @@ -293,15 +309,60 @@ errorToString error = withCurrentTime fn = - Time.now |> Task.perform fn + Effect.Time.now |> Effect.Task.perform fn + + +methodLoaderFrontend : + List + (Auth.Common.Method + frontendMsg + backendMsg + frontendModel + backendModel + FrontendOnly + toBackend + ) + -> Auth.Common.MethodId + -> + Maybe + (Auth.Common.Method + frontendMsg + backendMsg + frontendModel + backendModel + FrontendOnly + toBackend + ) +methodLoaderFrontend methods methodId = + methods + |> List.find + (\cfg -> + case cfg of + Auth.Common.ProtocolEmailMagicLink method -> + method.id == methodId + + Auth.Common.ProtocolOAuth method -> + method.id == methodId + ) -methodLoader : List (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel) -> Auth.Common.MethodId -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel) -methodLoader methods methodId = +methodLoaderBackend : + List + (Auth.Common.Method + frontendMsg + backendMsg + frontendModel + backendModel + BackendOnly + toBackend + ) + -> Auth.Common.MethodId + -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel BackendOnly toBackend) +methodLoaderBackend methods methodId = methods |> List.find - (\config -> - case config of + (\cfg -> + case cfg of Auth.Common.ProtocolEmailMagicLink method -> method.id == methodId @@ -312,7 +373,7 @@ methodLoader methods methodId = findMethod : Auth.Common.MethodId - -> Auth.Common.Config frontendMsg toBackend backendMsg toFrontend frontendModel backendModel - -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel) + -> Auth.Common.Config frontendMsg toBackend backendMsg toFrontend frontendModel backendModel toMsg + -> Maybe (Auth.Common.Method frontendMsg backendMsg frontendModel backendModel FrontendOnly toMsg) findMethod methodId config = - methodLoader config.methods methodId + methodLoaderFrontend config.methods methodId diff --git a/src/Auth/HttpHelpers.elm b/src/Auth/HttpHelpers.elm index 2c7cb7f..e801f17 100644 --- a/src/Auth/HttpHelpers.elm +++ b/src/Auth/HttpHelpers.elm @@ -2,7 +2,7 @@ module Auth.HttpHelpers exposing (..) -- import Effect.Http exposing (..) -import Http +import Effect.Http import Json.Decode as D @@ -48,22 +48,22 @@ parseError = D.decodeString (D.field "error" D.string) >> Result.toMaybe -httpErrorToString : Http.Error -> String +httpErrorToString : Effect.Http.Error -> String httpErrorToString err = case err of - Http.BadUrl url -> + Effect.Http.BadUrl url -> "HTTP malformed url: " ++ url - Http.Timeout -> + Effect.Http.Timeout -> "HTTP timeout exceeded" - Http.NetworkError -> + Effect.Http.NetworkError -> "HTTP network error" - Http.BadStatus code -> + Effect.Http.BadStatus code -> "Unexpected HTTP response code: " ++ String.fromInt code - Http.BadBody text -> + Effect.Http.BadBody text -> "HTTP error: " ++ text @@ -87,9 +87,9 @@ httpErrorToString err = -- "HTTP error: " ++ text -customError : String -> Http.Error +customError : String -> Effect.Http.Error customError s = - Http.BadBody <| "Error: " ++ s + Effect.Http.BadBody <| "Error: " ++ s @@ -98,27 +98,27 @@ customError s = -- Effect.Http.BadBody <| "Error: " ++ s -jsonResolver : D.Decoder a -> Http.Resolver Http.Error a +jsonResolver : D.Decoder a -> Effect.Http.Resolver restriction Effect.Http.Error a jsonResolver decoder = - Http.stringResolver <| + Effect.Http.stringResolver <| \response -> case response of - Http.GoodStatus_ _ body -> + Effect.Http.GoodStatus_ _ body -> D.decodeString decoder body |> Result.mapError D.errorToString - |> Result.mapError Http.BadBody + |> Result.mapError Effect.Http.BadBody - Http.BadUrl_ message -> - Err (Http.BadUrl message) + Effect.Http.BadUrl_ message -> + Err (Effect.Http.BadUrl message) - Http.Timeout_ -> - Err Http.Timeout + Effect.Http.Timeout_ -> + Err Effect.Http.Timeout - Http.NetworkError_ -> - Err Http.NetworkError + Effect.Http.NetworkError_ -> + Err Effect.Http.NetworkError - Http.BadStatus_ metadata body -> - Err (Http.BadBody (String.fromInt metadata.statusCode ++ ": " ++ body)) + Effect.Http.BadStatus_ metadata body -> + Err (Effect.Http.BadBody (String.fromInt metadata.statusCode ++ ": " ++ body)) diff --git a/src/Auth/Method/EmailMagicLink.elm b/src/Auth/Method/EmailMagicLink.elm index 8aabf4b..c5f55dd 100644 --- a/src/Auth/Method/EmailMagicLink.elm +++ b/src/Auth/Method/EmailMagicLink.elm @@ -1,19 +1,13 @@ module Auth.Method.EmailMagicLink exposing (..) import Auth.Common exposing (..) -import Base64.Encode as Base64 -import Bytes exposing (Bytes) -import Bytes.Encode as Bytes -import Dict exposing (Dict) -import Http -import Json.Decode as Json -import List.Extra as List -import OAuth -import OAuth.AuthorizationCode as OAuth -import Task exposing (Task) -import Time +import Effect.Browser.Navigation +import Effect.Command as Command exposing (BackendOnly, Command) +import Effect.Lamdera exposing (ClientId, SessionId) +import Effect.Task +import Effect.Time +import SeqDict exposing (SeqDict) import Url exposing (Protocol(..), Url) -import Url.Builder import Url.Parser exposing ((), ()) import Url.Parser.Query as Query @@ -22,38 +16,47 @@ configuration : { initiateSignin : SessionId -> ClientId - -> backendModel + -> { backendModel | pendingAuths : SeqDict SessionId PendingAuth } -> { username : Maybe String } - -> Time.Posix - -> ( backendModel, Cmd backendMsg ) + -> Effect.Time.Posix + -> ( { backendModel | pendingAuths : SeqDict SessionId PendingAuth }, Command BackendOnly toMsg backendMsg ) , onAuthCallbackReceived : SessionId -> ClientId -> Url -> AuthCode -> State - -> Time.Posix + -> Effect.Time.Posix -> (BackendMsg -> backendMsg) - -> backendModel - -> ( backendModel, Cmd backendMsg ) + -> { backendModel | pendingAuths : SeqDict SessionId PendingAuth } + -> ( { backendModel | pendingAuths : SeqDict SessionId PendingAuth }, Command BackendOnly toMsg backendMsg ) } -> Method frontendMsg backendMsg { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url } - backendModel + { backendModel | pendingAuths : SeqDict SessionId PendingAuth } + BackendOnly + toMsg configuration { initiateSignin, onAuthCallbackReceived } = ProtocolEmailMagicLink { id = "EmailMagicLink" , initiateSignin = initiateSignin , onFrontendCallbackInit = onFrontendCallbackInit , onAuthCallbackReceived = onAuthCallbackReceived - , placeholder = \frontendMsg backendMsg frontendModel backendModel -> () + , placeholder = \_ _ _ _ -> () } -onFrontendCallbackInit frontendModel methodId origin key toBackend = +onFrontendCallbackInit : + { frontendModel | authFlow : Auth.Common.Flow } + -> MethodId + -> Url + -> Effect.Browser.Navigation.Key + -> (ToBackend -> Command restriction toMsg frontendMsg) + -> ( { frontendModel | authFlow : Auth.Common.Flow }, Command restriction toMsg frontendMsg ) +onFrontendCallbackInit frontendModel methodId origin _ toBackend = case origin |> Url.Parser.parse (callbackUrl methodId queryParams) of Just ( Just token, Just email ) -> ( { frontendModel | authFlow = Auth.Common.Pending } @@ -62,12 +65,12 @@ onFrontendCallbackInit frontendModel methodId origin key toBackend = _ -> ( { frontendModel | authFlow = Errored <| ErrAuthString "Missing token and/or email parameters. Please try again." } - , Cmd.none + , Command.none ) trigger msg = - Time.now |> Task.perform (always msg) + Effect.Time.now |> Effect.Task.perform (always msg) callbackUrl methodId = diff --git a/src/Auth/Method/OAuthAuth0.elm b/src/Auth/Method/OAuthAuth0.elm index a086b68..edf95b4 100644 --- a/src/Auth/Method/OAuthAuth0.elm +++ b/src/Auth/Method/OAuthAuth0.elm @@ -6,13 +6,14 @@ import Auth.Protocol.OAuth import Base64.Encode as Base64 import Bytes exposing (Bytes) import Bytes.Encode as Bytes -import Dict exposing (Dict) -import Http +import Effect.Command exposing (FrontendOnly) +import Effect.Http +import Effect.Task exposing (Task) import JWT exposing (..) import JWT.JWS as JWS import Json.Decode as Json import OAuth.AuthorizationCode as OAuth -import Task exposing (Task) +import SeqDict as Dict exposing (SeqDict) import Url exposing (Protocol(..), Url) @@ -26,6 +27,8 @@ configuration : backendMsg { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url } backendModel + FrontendOnly + toMsg configuration clientId clientSecret appTenant = ProtocolOAuth { id = "OAuthAuth0" @@ -55,28 +58,36 @@ configuration clientId clientSecret appTenant = getUserInfo : OAuth.AuthenticationSuccess - -> Task Auth.Common.Error UserInfo + -> Effect.Task.Task restriction Auth.Common.Error UserInfo getUserInfo authenticationSuccess = let - extract : String -> Json.Decoder a -> Dict String Json.Value -> Result String a + extract : String -> Json.Decoder a -> SeqDict String Json.Value -> Result String a extract k d v = Dict.get k v |> Maybe.map - (\v_ -> - Json.decodeValue d v_ - |> Result.mapError Json.errorToString + (\value -> + case Json.decodeValue d value of + Ok decoded -> + Ok decoded + + Err err -> + Err <| Json.errorToString err ) |> Maybe.withDefault (Err <| "Key " ++ k ++ " not found") - extractOptional : a -> String -> Json.Decoder a -> Dict String Json.Value -> Result String a + extractOptional : a -> String -> Json.Decoder a -> SeqDict String Json.Value -> Result String a extractOptional default k d v = Dict.get k v |> Maybe.map - (\v_ -> - Json.decodeValue d v_ - |> Result.mapError Json.errorToString + (\value -> + case Json.decodeValue d value of + Ok decoded -> + Ok decoded + + Err _ -> + Ok default ) - |> Maybe.withDefault (Ok <| default) + |> Maybe.withDefault (Ok default) tokenR = case authenticationSuccess.idJwt of @@ -113,10 +124,10 @@ getUserInfo authenticationSuccess = (extractOptional Nothing "family_name" (Json.string |> Json.nullable) meta) ) in - Task.mapError (Auth.Common.ErrAuthString << HttpHelpers.httpErrorToString) <| + Effect.Task.mapError (Auth.Common.ErrAuthString << HttpHelpers.httpErrorToString) <| case stuff of Ok result -> - Task.succeed + Effect.Task.succeed { email = result.email , name = [ Maybe.withDefault "" result.given_name, Maybe.withDefault "" result.family_name ] @@ -126,7 +137,7 @@ getUserInfo authenticationSuccess = } Err err -> - Task.fail (Http.BadBody err) + Effect.Task.fail (Effect.Http.BadBody err) jwtErrorToString err = diff --git a/src/Auth/Method/OAuthGithub.elm b/src/Auth/Method/OAuthGithub.elm index b065e64..3b8c439 100644 --- a/src/Auth/Method/OAuthGithub.elm +++ b/src/Auth/Method/OAuthGithub.elm @@ -4,16 +4,17 @@ import Auth.Common exposing (..) import Auth.HttpHelpers as HttpHelpers import Auth.Protocol.OAuth import Base64.Encode as Base64 -import Browser.Navigation as Navigation import Bytes exposing (Bytes) import Bytes.Encode as Bytes -import Http +import Effect.Browser.Navigation as Navigation +import Effect.Command exposing (FrontendOnly) +import Effect.Http +import Effect.Task exposing (Task) import Json.Decode as Json import Json.Decode.Pipeline exposing (..) import List.Extra as List import OAuth import OAuth.AuthorizationCode as OAuth -import Task exposing (Task) import Url exposing (Protocol(..), Url) import Url.Builder @@ -27,6 +28,8 @@ configuration : backendMsg { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url } backendModel + FrontendOnly + toMsg configuration clientId clientSecret = ProtocolOAuth { id = "OAuthGithub" @@ -47,43 +50,43 @@ configuration clientId clientSecret = getUserInfo : OAuth.AuthenticationSuccess - -> Task Auth.Common.Error UserInfo + -> Effect.Task.Task restriction Auth.Common.Error UserInfo getUserInfo authenticationSuccess = getUserInfoTask authenticationSuccess - |> Task.andThen + |> Effect.Task.andThen (\userInfo -> if userInfo.email == "" then fallbackGetEmailFromEmails authenticationSuccess userInfo else - Task.succeed userInfo + Effect.Task.succeed userInfo ) -fallbackGetEmailFromEmails : OAuth.AuthenticationSuccess -> UserInfo -> Task Auth.Common.Error UserInfo +fallbackGetEmailFromEmails : OAuth.AuthenticationSuccess -> UserInfo -> Effect.Task.Task restriction Auth.Common.Error UserInfo fallbackGetEmailFromEmails authenticationSuccess userInfo = getUserEmailsTask authenticationSuccess - |> Task.andThen + |> Effect.Task.andThen (\userEmails -> case userEmails |> List.find (\v -> v.primary == True) of Just record -> - Task.succeed { userInfo | email = record.email } + Effect.Task.succeed { userInfo | email = record.email } Nothing -> - Task.fail <| + Effect.Task.fail <| HttpHelpers.customError "Could not retrieve an email from Github profile or emails list." ) - |> Task.mapError (HttpHelpers.httpErrorToString >> Auth.Common.ErrAuthString) + |> Effect.Task.mapError (HttpHelpers.httpErrorToString >> Auth.Common.ErrAuthString) -getUserInfoTask : OAuth.AuthenticationSuccess -> Task Auth.Common.Error UserInfo +getUserInfoTask : OAuth.AuthenticationSuccess -> Effect.Task.Task restriction Auth.Common.Error UserInfo getUserInfoTask authenticationSuccess = - Http.task + Effect.Http.task { method = "GET" , headers = OAuth.useToken authenticationSuccess.token [] , url = Url.toString { defaultHttpsUrl | host = "api.github.com", path = "/user" } - , body = Http.emptyBody + , body = Effect.Http.emptyBody , resolver = HttpHelpers.jsonResolver (Json.succeed UserInfo @@ -93,7 +96,7 @@ getUserInfoTask authenticationSuccess = ) , timeout = Nothing } - |> Task.mapError (HttpHelpers.httpErrorToString >> Auth.Common.ErrAuthString) + |> Effect.Task.mapError (HttpHelpers.httpErrorToString >> Auth.Common.ErrAuthString) decodeNonEmptyString : Json.Decoder (Maybe String) @@ -105,13 +108,13 @@ type alias GithubEmail = { primary : Bool, email : String } -getUserEmailsTask : OAuth.AuthenticationSuccess -> Task Http.Error (List GithubEmail) +getUserEmailsTask : OAuth.AuthenticationSuccess -> Effect.Task.Task restriction Effect.Http.Error (List GithubEmail) getUserEmailsTask authenticationSuccess = - Http.task + Effect.Http.task { method = "GET" , headers = OAuth.useToken authenticationSuccess.token [] , url = Url.toString { defaultHttpsUrl | host = "api.github.com", path = "/user/emails" } - , body = Http.emptyBody + , body = Effect.Http.emptyBody , resolver = HttpHelpers.jsonResolver (Json.list diff --git a/src/Auth/Method/OAuthGoogle.elm b/src/Auth/Method/OAuthGoogle.elm index 3f3a99d..d15f1f6 100644 --- a/src/Auth/Method/OAuthGoogle.elm +++ b/src/Auth/Method/OAuthGoogle.elm @@ -3,19 +3,16 @@ module Auth.Method.OAuthGoogle exposing (..) import Auth.Common exposing (..) import Auth.HttpHelpers as HttpHelpers import Auth.Protocol.OAuth -import Base64.Encode as Base64 -import Bytes exposing (Bytes) -import Bytes.Encode as Bytes -import Dict exposing (Dict) -import Http +import Effect.Command exposing (BackendOnly) +import Effect.Http +import Effect.Task import JWT exposing (..) import JWT.JWS as JWS import Json.Decode as Json import OAuth import OAuth.AuthorizationCode as OAuth -import Task exposing (Task) +import SeqDict as Dict exposing (SeqDict) import Url exposing (Protocol(..), Url) -import Url.Builder configuration : @@ -27,6 +24,8 @@ configuration : backendMsg { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url } backendModel + BackendOnly + toMsg configuration clientId clientSecret = ProtocolOAuth { id = "OAuthGoogle" @@ -39,7 +38,7 @@ configuration clientId clientSecret = , scope = [ "openid email profile" ] , getUserInfo = getUserInfo , onFrontendCallbackInit = Auth.Protocol.OAuth.onFrontendCallbackInit - , placeholder = \x -> () + , placeholder = \_ -> () -- , onAuthCallbackReceived = Debug.todo "onAuthCallbackReceived" } @@ -47,10 +46,10 @@ configuration clientId clientSecret = getUserInfo : OAuth.AuthenticationSuccess - -> Task Auth.Common.Error UserInfo + -> Effect.Task.Task restriction Auth.Common.Error UserInfo getUserInfo authenticationSuccess = let - extract : String -> Json.Decoder a -> Dict String Json.Value -> Result String a + extract : String -> Json.Decoder a -> SeqDict String Json.Value -> Result String a extract k d v = Dict.get k v |> Maybe.map @@ -60,7 +59,7 @@ getUserInfo authenticationSuccess = ) |> Maybe.withDefault (Err <| "Key " ++ k ++ " not found") - extractOptional : a -> String -> Json.Decoder a -> Dict String Json.Value -> Result String a + extractOptional : a -> String -> Json.Decoder a -> SeqDict String Json.Value -> Result String a extractOptional default k d v = Dict.get k v |> Maybe.map @@ -105,10 +104,10 @@ getUserInfo authenticationSuccess = (extractOptional Nothing "family_name" (Json.string |> Json.nullable) meta) ) in - Task.mapError (Auth.Common.ErrAuthString << HttpHelpers.httpErrorToString) <| + Effect.Task.mapError (Auth.Common.ErrAuthString << HttpHelpers.httpErrorToString) <| case stuff of Ok result -> - Task.succeed + Effect.Task.succeed { email = result.email , name = [ result.given_name, Maybe.withDefault "" result.family_name ] @@ -118,7 +117,7 @@ getUserInfo authenticationSuccess = } Err err -> - Task.fail (Http.BadBody err) + Effect.Task.fail (Effect.Http.BadBody err) jwtErrorToString err = diff --git a/src/Auth/Protocol/OAuth.elm b/src/Auth/Protocol/OAuth.elm index 4efcdf7..9586152 100644 --- a/src/Auth/Protocol/OAuth.elm +++ b/src/Auth/Protocol/OAuth.elm @@ -2,16 +2,18 @@ module Auth.Protocol.OAuth exposing (..) import Auth.Common exposing (..) import Auth.HttpHelpers as HttpHelpers -import Browser.Navigation as Navigation -import Dict exposing (Dict) -import Http +import Duration +import Effect.Browser.Navigation as Navigation +import Effect.Command as Command exposing (BackendOnly, Command, FrontendOnly) +import Effect.Http +import Effect.Lamdera exposing (SessionId, sessionIdToString) +import Effect.Task exposing (Task) +import Effect.Time import Json.Decode as Json import OAuth import OAuth.AuthorizationCode as OAuth -import Process import SHA1 -import Task exposing (Task) -import Time +import SeqDict as Dict exposing (SeqDict) import Url exposing (Url) @@ -20,8 +22,8 @@ onFrontendCallbackInit : -> Auth.Common.MethodId -> Url -> Navigation.Key - -> (Auth.Common.ToBackend -> Cmd frontendMsg) - -> ( { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url }, Cmd frontendMsg ) + -> (Auth.Common.ToBackend -> Command FrontendOnly toMsg frontendMsg) + -> ( { frontendModel | authFlow : Flow, authRedirectBaseUrl : Url }, Command FrontendOnly toMsg frontendMsg ) onFrontendCallbackInit model methodId origin navigationKey toBackendFn = let redirectUri = @@ -33,7 +35,7 @@ onFrontendCallbackInit model methodId origin navigationKey toBackendFn = case OAuth.parseCode origin of OAuth.Empty -> ( { model | authFlow = Idle } - , Cmd.none + , Command.none ) OAuth.Success { code, state } -> @@ -48,7 +50,7 @@ onFrontendCallbackInit model methodId origin navigationKey toBackendFn = accessTokenRequested model_ methodId code state_ in ( newModel - , Cmd.batch [ toBackendFn newCmds, clearUrl ] + , Command.batch [ toBackendFn newCmds, clearUrl ] ) OAuth.Error error -> @@ -69,15 +71,16 @@ accessTokenRequested model methodId code state = ) +initiateSignin : Bool -> SessionId -> Url -> ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel restriction toMsg -> (BackendMsg -> backendMsg) -> Effect.Time.Posix -> { b | pendingAuths : SeqDict Effect.Lamdera.SessionId PendingAuth } -> ( { b | pendingAuths : SeqDict Effect.Lamdera.SessionId PendingAuth }, Command BackendOnly toMsg backendMsg ) initiateSignin isDev sessionId baseUrl config asBackendMsg now backendModel = let signedState = SHA1.toBase64 <| SHA1.fromString <| - (String.fromInt <| Time.posixToMillis <| now) + (String.fromInt <| Effect.Time.posixToMillis <| now) -- @TODO this needs to be user-injected config ++ "0x3vd7a" - ++ sessionId + ++ sessionIdToString sessionId newPendingAuth : PendingAuth newPendingAuth = @@ -103,7 +106,7 @@ initiateSignin isDev sessionId baseUrl config asBackendMsg now backendModel = ) -generateSigninUrl : Url -> Auth.Common.State -> Auth.Common.ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel -> Url +generateSigninUrl : Url -> Auth.Common.State -> Auth.Common.ConfigurationOAuth frontendMsg backendMsg frontendModel backendModel restriction toMsg -> Url generateSigninUrl baseUrl state configuration = let queryAdjustedUrl = @@ -126,10 +129,11 @@ generateSigninUrl baseUrl state configuration = |> OAuth.makeAuthorizationUrl +onAuthCallbackReceived : SessionId -> Effect.Lamdera.ClientId -> { a | clientId : String, clientSecret : String, tokenEndpoint : Url, id : MethodId, getUserInfo : OAuth.AuthenticationSuccess -> Task BackendOnly Error UserInfo } -> Url -> OAuth.AuthorizationCode -> String -> Effect.Time.Posix -> (BackendMsg -> backendMsg) -> { backendModel | pendingAuths : SeqDict SessionId PendingAuth } -> ( { backendModel | pendingAuths : SeqDict SessionId PendingAuth }, Command BackendOnly toMsg backendMsg ) onAuthCallbackReceived sessionId clientId method receivedUrl code state now asBackendMsg backendModel = ( backendModel , validateCallbackToken method.clientId method.clientSecret method.tokenEndpoint receivedUrl code - |> Task.andThen + |> Effect.Task.andThen (\authenticationResponse -> case backendModel.pendingAuths |> Dict.get sessionId of Just pendingAuth -> @@ -140,15 +144,15 @@ onAuthCallbackReceived sessionId clientId method receivedUrl code state now asBa if pendingAuth.state == state then method.getUserInfo authenticationResponse - |> Task.map (\userInfo -> ( userInfo, authToken )) + |> Effect.Task.map (\userInfo -> ( userInfo, authToken )) else - Task.fail <| Auth.Common.ErrAuthString "Invalid auth state. Please log in again or report this issue." + Effect.Task.fail <| Auth.Common.ErrAuthString "Invalid auth state. Please log in again or report this issue." Nothing -> - Task.fail <| Auth.Common.ErrAuthString "Couldn't validate auth, please login again." + Effect.Task.fail <| Auth.Common.ErrAuthString "Couldn't validate auth, please login again." ) - |> Task.attempt (Auth.Common.AuthSuccess sessionId clientId method.id now >> asBackendMsg) + |> Effect.Task.attempt (Auth.Common.AuthSuccess sessionId clientId method.id now >> asBackendMsg) ) @@ -158,7 +162,7 @@ validateCallbackToken : -> Url -> Url -> OAuth.AuthorizationCode - -> Task Auth.Common.Error OAuth.AuthenticationSuccess + -> Effect.Task.Task BackendOnly Auth.Common.Error OAuth.AuthenticationSuccess validateCallbackToken clientId clientSecret tokenEndpoint redirectUri code = let req = @@ -173,20 +177,20 @@ validateCallbackToken clientId clientSecret tokenEndpoint redirectUri code = } in { method = req.method - , headers = req.headers ++ [ Http.header "Accept" "application/json" ] + , headers = req.headers ++ [ Effect.Http.header "Accept" "application/json" ] , url = req.url , body = req.body , resolver = HttpHelpers.jsonResolver OAuth.defaultAuthenticationSuccessDecoder - , timeout = req.timeout + , timeout = req.timeout |> Maybe.map Duration.milliseconds } - |> Http.task - |> Task.mapError parseAuthenticationResponseError + |> Effect.Http.task + |> Effect.Task.mapError parseAuthenticationResponseError -parseAuthenticationResponse : Result Http.Error OAuth.AuthenticationSuccess -> Result Auth.Common.Error OAuth.AuthenticationSuccess +parseAuthenticationResponse : Result Effect.Http.Error OAuth.AuthenticationSuccess -> Result Auth.Common.Error OAuth.AuthenticationSuccess parseAuthenticationResponse res = case res of - Err (Http.BadBody body) -> + Err (Effect.Http.BadBody body) -> case Json.decodeString OAuth.defaultAuthenticationErrorDecoder body of Ok error -> Err <| Auth.Common.ErrAuthentication error @@ -201,10 +205,10 @@ parseAuthenticationResponse res = Ok authenticationSuccess -parseAuthenticationResponseError : Http.Error -> Auth.Common.Error +parseAuthenticationResponseError : Effect.Http.Error -> Auth.Common.Error parseAuthenticationResponseError httpErr = case httpErr of - Http.BadBody body -> + Effect.Http.BadBody body -> case Json.decodeString OAuth.defaultAuthenticationErrorDecoder body of Ok error -> Auth.Common.ErrAuthentication error @@ -216,14 +220,14 @@ parseAuthenticationResponseError httpErr = Auth.Common.ErrHTTPGetAccessToken -makeToken : Auth.Common.MethodId -> OAuth.AuthenticationSuccess -> Time.Posix -> Auth.Common.Token +makeToken : Auth.Common.MethodId -> OAuth.AuthenticationSuccess -> Effect.Time.Posix -> Auth.Common.Token makeToken methodId authenticationSuccess now = { methodId = methodId , token = authenticationSuccess.token , created = now , expires = - (Time.posixToMillis now + (Effect.Time.posixToMillis now + ((authenticationSuccess.expiresIn |> Maybe.withDefault 0) * 1000) ) - |> Time.millisToPosix + |> Effect.Time.millisToPosix } diff --git a/src/JWT.elm b/src/JWT.elm index 0c1dfaf..ac17762 100644 --- a/src/JWT.elm +++ b/src/JWT.elm @@ -17,10 +17,10 @@ module JWT exposing -} +import Effect.Task exposing (Task) +import Effect.Time exposing (Posix) import JWT.ClaimSet exposing (VerifyOptions) import JWT.JWS as JWS -import Task exposing (Task) -import Time exposing (Posix) {-| A JSON Web Token. @@ -67,7 +67,7 @@ type VerificationError {-| Check if the token is valid. -} -isValid : VerifyOptions -> String -> Posix -> JWT -> Result VerificationError Bool +isValid : VerifyOptions -> String -> Effect.Time.Posix -> JWT -> Result VerificationError Bool isValid options key now token = case token of JWS token_ -> @@ -77,7 +77,7 @@ isValid options key now token = {-| A task to check if the token is valid. -} -validate : VerifyOptions -> String -> JWT -> Task Never (Result VerificationError Bool) +validate : VerifyOptions -> String -> JWT -> Effect.Task.Task restriction Never (Result VerificationError Bool) validate options key token = - Time.now - |> Task.andThen ((\now -> isValid options key now token) >> Task.succeed) + Effect.Time.now + |> Effect.Task.andThen ((\now -> isValid options key now token) >> Effect.Task.succeed) diff --git a/src/JWT/ClaimSet.elm b/src/JWT/ClaimSet.elm index 5d45ca8..1f1bec0 100644 --- a/src/JWT/ClaimSet.elm +++ b/src/JWT/ClaimSet.elm @@ -1,10 +1,10 @@ module JWT.ClaimSet exposing (ClaimSet, VerificationError(..), VerifyOptions, decoder, encoder, isValid) -import Dict exposing (Dict) -import Json.Decode as Decode +import Effect.Time exposing (Posix) +import Json.Decode as Decode exposing (Decoder) import Json.Decode.Pipeline exposing (custom, optional) import Json.Encode as Encode -import Time exposing (Posix) +import SeqDict as Dict exposing (SeqDict) type alias ClaimSet = @@ -15,10 +15,15 @@ type alias ClaimSet = , nbf : Maybe Int , iat : Maybe Int , jti : Maybe String - , metadata : Dict String Decode.Value + , metadata : SeqDict String Decode.Value } +decoderDict : Decoder a -> Decoder (SeqDict String a) +decoderDict decoder_ = + Decode.map Dict.fromList (Decode.keyValuePairs decoder_) + + decoder : Decode.Decoder ClaimSet decoder = Decode.succeed ClaimSet @@ -29,7 +34,7 @@ decoder = |> optional "nbf" (Decode.maybe Decode.int) Nothing |> optional "iat" (Decode.maybe Decode.int) Nothing |> optional "jti" (Decode.maybe Decode.string) Nothing - |> custom (Decode.dict Decode.value) + |> custom (decoderDict Decode.value) encoder : ClaimSet -> Encode.Value @@ -71,7 +76,7 @@ type alias VerifyOptions = } -isValid : VerifyOptions -> Posix -> ClaimSet -> Result VerificationError Bool +isValid : VerifyOptions -> Effect.Time.Posix -> ClaimSet -> Result VerificationError Bool isValid options now claims = checkIssuer claims.iss options.issuer |> Result.andThen @@ -144,42 +149,42 @@ checkID claim option = Err InvalidJWTID -checkExpiration : Posix -> Int -> Maybe Int -> Result VerificationError Bool +checkExpiration : Effect.Time.Posix -> Int -> Maybe Int -> Result VerificationError Bool checkExpiration now leeway claim = case claim of Nothing -> Ok True Just expiration -> - if Time.posixToMillis now - leeway < expiration * 1000 then + if Effect.Time.posixToMillis now - leeway < expiration * 1000 then Ok True else Err Expired -checkNotBefore : Posix -> Int -> Maybe Int -> Result VerificationError Bool +checkNotBefore : Effect.Time.Posix -> Int -> Maybe Int -> Result VerificationError Bool checkNotBefore now leeway claim = case claim of Nothing -> Ok True Just nbf -> - if Time.posixToMillis now + leeway > nbf * 1000 then + if Effect.Time.posixToMillis now + leeway > nbf * 1000 then Ok True else Err NotYetValid -checkIssuedAt : Posix -> Int -> Maybe Int -> Result VerificationError Bool +checkIssuedAt : Effect.Time.Posix -> Int -> Maybe Int -> Result VerificationError Bool checkIssuedAt now leeway claim = case claim of Nothing -> Ok True Just iat -> - if Time.posixToMillis now + leeway > iat * 1000 then + if Effect.Time.posixToMillis now + leeway > iat * 1000 then Ok True else diff --git a/src/JWT/JWS.elm b/src/JWT/JWS.elm index 73541e8..9745923 100644 --- a/src/JWT/JWS.elm +++ b/src/JWT/JWS.elm @@ -5,6 +5,7 @@ import Base64.Encode as B64Encode import Bytes exposing (Bytes) import Bytes.Decode import Crypto.HMAC +import Effect.Time exposing (Posix) import JWT.ClaimSet as ClaimSet exposing (VerifyOptions) import JWT.JWK as JWK import JWT.UrlBase64 as UrlBase64 @@ -12,7 +13,6 @@ import Json.Decode as JDecode import Json.Decode.Pipeline exposing (optional, required) import Json.Encode as JEncode import Result exposing (andThen, map, mapError) -import Time exposing (Posix) import Word.Bytes @@ -141,7 +141,7 @@ type VerificationError | ClaimSet ClaimSet.VerificationError -isValid : VerifyOptions -> String -> Posix -> JWS -> Result VerificationError Bool +isValid : VerifyOptions -> String -> Effect.Time.Posix -> JWS -> Result VerificationError Bool isValid options key now token = checkSignature key token |> Result.andThen diff --git a/src/JWT/UrlBase64.elm b/src/JWT/UrlBase64.elm index 410584a..1448393 100644 --- a/src/JWT/UrlBase64.elm +++ b/src/JWT/UrlBase64.elm @@ -39,7 +39,7 @@ of the decoders underneath. -} -import Maybe as Maybe +import Maybe import Regex exposing (Regex) diff --git a/src/OAuth.elm b/src/OAuth.elm index 95006d6..135b222 100644 --- a/src/OAuth.elm +++ b/src/OAuth.elm @@ -62,8 +62,8 @@ The following section can be ignored if you're dealing with a very generic OAuth -} +import Effect.Http import Extra.Maybe as Maybe -import Http as Http @@ -98,9 +98,9 @@ type alias TokenString = {-| Use a token to authenticate a request. -} -useToken : Token -> List Http.Header -> List Http.Header +useToken : Token -> List Effect.Http.Header -> List Effect.Http.Header useToken token = - (::) (Http.header "Authorization" (tokenToString token)) + (::) (Effect.Http.header "Authorization" (tokenToString token)) {-| Create a token from two string representing a token type and diff --git a/src/OAuth/AuthorizationCode.elm b/src/OAuth/AuthorizationCode.elm index 4771416..4bf11d1 100644 --- a/src/OAuth/AuthorizationCode.elm +++ b/src/OAuth/AuthorizationCode.elm @@ -102,8 +102,8 @@ request. -} -import Dict as Dict exposing (Dict) -import Http +import Dict exposing (Dict) +import Effect.Http import Json.Decode as Json import OAuth exposing (ErrorCode, GrantType(..), ResponseType(..), Token, errorCodeFromString, grantTypeToString) import OAuth.Internal as Internal exposing (..) @@ -331,10 +331,10 @@ in order to create a new request and may be adjusted at will. -} type alias RequestParts a = { method : String - , headers : List Http.Header + , headers : List Effect.Http.Header , url : String - , body : Http.Body - , expect : Http.Expect a + , body : Effect.Http.Body + , expect : Effect.Http.Expect a , timeout : Maybe Float , tracker : Maybe String } @@ -362,7 +362,7 @@ type alias Credentials = req = makeTokenRequest toMsg authentication |> Http.request -} -makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg +makeTokenRequest : (Result Effect.Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg makeTokenRequest = makeTokenRequestWith AuthorizationCode defaultAuthenticationSuccessDecoder Dict.empty @@ -413,7 +413,7 @@ type and extra fields to be set on the query. Dict.empty -} -makeTokenRequestWith : GrantType -> Json.Decoder success -> Dict String String -> (Result Http.Error success -> msg) -> Authentication -> RequestParts msg +makeTokenRequestWith : GrantType -> Json.Decoder success -> Dict String String -> (Result Effect.Http.Error success -> msg) -> Authentication -> RequestParts msg makeTokenRequestWith grantType decoder extraFields toMsg { credentials, code, url, redirectUri } = let body = diff --git a/src/OAuth/Internal.elm b/src/OAuth/Internal.elm index 590fd21..c517694 100644 --- a/src/OAuth/Internal.elm +++ b/src/OAuth/Internal.elm @@ -38,8 +38,8 @@ module OAuth.Internal exposing ) import Base64.Encode as Base64 -import Dict as Dict exposing (Dict) -import Http +import Dict exposing (Dict) +import Effect.Http import Json.Decode as Json import OAuth exposing (..) import Url exposing (Protocol(..), Url) @@ -294,23 +294,23 @@ makeAuthorizationUrl responseType extraFields { clientId, url, redirectUri, scop { url | query = Just (baseQuery ++ "&" ++ query) } -makeRequest : Json.Decoder success -> (Result Http.Error success -> msg) -> Url -> List Http.Header -> String -> RequestParts msg +makeRequest : Json.Decoder success -> (Result Effect.Http.Error success -> msg) -> Url -> List Effect.Http.Header -> String -> RequestParts msg makeRequest decoder toMsg url headers body = { method = "POST" , headers = headers , url = Url.toString url - , body = Http.stringBody "application/x-www-form-urlencoded" body - , expect = Http.expectJson toMsg decoder + , body = Effect.Http.stringBody "application/x-www-form-urlencoded" body + , expect = Effect.Http.expectJson toMsg decoder , timeout = Nothing , tracker = Nothing } -makeHeaders : Maybe { clientId : String, secret : String } -> List Http.Header +makeHeaders : Maybe { clientId : String, secret : String } -> List Effect.Http.Header makeHeaders credentials = credentials |> Maybe.map (\{ clientId, secret } -> Base64.encode <| Base64.string <| (clientId ++ ":" ++ secret)) - |> Maybe.map (\s -> [ Http.header "Authorization" ("Basic " ++ s) ]) + |> Maybe.map (\s -> [ Effect.Http.header "Authorization" ("Basic " ++ s) ]) |> Maybe.withDefault [] @@ -371,10 +371,10 @@ extractTokenString = type alias RequestParts a = { method : String - , headers : List Http.Header + , headers : List Effect.Http.Header , url : String - , body : Http.Body - , expect : Http.Expect a + , body : Effect.Http.Body + , expect : Effect.Http.Expect a , timeout : Maybe Float , tracker : Maybe String }