truqu / elm-oauth2 / OAuth.AuthorizationCode.PKCE

OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack. A possible mitigation against the threat is to use a technique called Proof Key for Code Exchange (PKCE, pronounced "pixy") when supported by the target authorization server. See also RFC 7636.

Quick Start

To get started, have a look at the live-demo and its corresponding source code

Overview

                                     +-----------------+
                                     |  Auth   Server  |
    +-------+                        | +-------------+ |
    |       |--(1)- Auth Request --->| |             | |
    |       |    + code_challenge    | |    Auth     | |
    |       |                        | |   Endpoint  | |
    |       |<-(2)-- Auth Code ------| |             | |
    |  Elm  |                        | +-------------+ |
    |  App  |                        |                 |
    |       |                        | +-------------+ |
    |       |--(3)- Token Request -->| |             | |
    |       |      + code_verifier   | |   Token     | |
    |       |                        | |  Endpoint   | |
    |       |<-(4)- Access Token --->| |             | |
    +-------+                        | +-------------+ |
                                     +-----------------+

See also the Authorization Code flow for details about the basic version of this flow.

Code Verifier / Challenge


type CodeVerifier
    = CodeVerifier Base64.Encode.Encoder

An opaque type representing a code verifier. Typically constructed from a high quality entropy.

case codeVerifierFromBytes entropy of
  Nothing -> {- ...-}
  Just codeVerifier -> {- ... -}


type CodeChallenge
    = CodeChallenge Base64.Encode.Encoder

An opaque type representing a code challenge. Typically constructed from a CodeVerifier.

let codeChallenge = mkCodeChallenge codeVerifier

codeVerifierFromBytes : Bytes -> Maybe CodeVerifier

Construct a code verifier from a byte sequence generated from a high quality randomness source (i.e. cryptographic).

Ideally, the byte sequence should be 32 or 64 bytes, and it must be at least 32 bytes and at most 90 bytes.

codeVerifierToString : CodeVerifier -> String

Convert a code verifier to its string representation.

mkCodeChallenge : CodeVerifier -> CodeChallenge

Construct a CodeChallenge to send to the authorization server. Upon receiving the authorization code, the client can then the associated CodeVerifier to prove it is the rightful owner of the authorization code.

codeChallengeToString : CodeChallenge -> String

Convert a code challenge to its string representation.

Authorize

makeAuthorizationUrl : Authorization -> Url

Redirects the resource owner (user) to the resource provider server using the specified authorization flow.


type alias Authorization =
{ clientId : String
, url : Url
, redirectUri : Url
, scope : List String
, state : Maybe String
, codeChallenge : CodeChallenge 
}

Request configuration for an authorization (Authorization Code & Implicit flows)

parseCode : Url -> AuthorizationResult

Parse the location looking for a parameters set by the resource provider server after redirecting the resource owner (user).

Returns AuthorizationResult Empty when there's nothing.


type alias AuthorizationResult =
AuthorizationResultWith AuthorizationError AuthorizationSuccess

Describes errors coming from attempting to parse a url after an OAuth redirection


type alias AuthorizationError =
{ error : OAuth.ErrorCode
, errorDescription : Maybe String
, errorUri : Maybe String
, state : Maybe String 
}

Describes an OAuth error as a result of an authorization request failure


type alias AuthorizationSuccess =
{ code : String
, state : Maybe String 
}

The response obtained as a result of an authorization


type alias AuthorizationCode =
String

A simple type alias to ease readability of type signatures

Authenticate

makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg

Builds a the request components required to get a token from an authorization code

let req : Http.Request AuthenticationSuccess
    req = makeTokenRequest toMsg authentication |> Http.request


type alias Authentication =
{ credentials : Credentials
, code : String
, codeVerifier : CodeVerifier
, redirectUri : Url
, url : Url 
}

Request configuration for an AuthorizationCode authentication


type alias Credentials =
{ clientId : String
, secret : Maybe String 
}

Describes at least a clientId and if define, a complete set of credentials with the secret. The secret is so-to-speak optional and depends on whether the authorization server you interact with requires a Basic authentication on top of the authentication request. Provides it if you need to do so.

  { clientId = "<my-client-id>"
  , secret = Just "<my-client-secret>"
  }


type alias AuthenticationSuccess =
{ token : OAuth.Token
, refreshToken : Maybe OAuth.Token
, expiresIn : Maybe Basics.Int
, scope : List String 
}

The response obtained as a result of an authentication (implicit or not)


type alias AuthenticationError =
{ error : OAuth.ErrorCode
, errorDescription : Maybe String
, errorUri : Maybe String 
}

Describes an OAuth error as a result of a request failure


type alias RequestParts a =
{ method : String
, headers : List Http.Header
, url : String
, body : Http.Body
, expect : Http.Expect a
, timeout : Maybe Basics.Float
, tracker : Maybe String 
}

Parts required to build a request. This record is given to Http.request in order to create a new request and may be adjusted at will.

JSON Decoders

defaultAuthenticationSuccessDecoder : Json.Decode.Decoder AuthenticationSuccess

Json decoder for a positive response. You may provide a custom response decoder using other decoders from this module, or some of your own craft.

defaultAuthenticationSuccessDecoder : Decoder AuthenticationSuccess
defaultAuthenticationSuccessDecoder =
    D.map4 AuthenticationSuccess
        tokenDecoder
        refreshTokenDecoder
        expiresInDecoder
        scopeDecoder

defaultAuthenticationErrorDecoder : Json.Decode.Decoder AuthenticationError

Json decoder for an errored response.

case res of
    Err (Http.BadStatus { body }) ->
        case Json.decodeString OAuth.AuthorizationCode.defaultAuthenticationErrorDecoder body of
            Ok { error, errorDescription } ->
                doSomething

            _ ->
                parserFailed

    _ ->
        someOtherError

Custom Decoders & Parsers (advanced)

Authorize

makeAuthorizationUrlWith : OAuth.ResponseType -> Dict String String -> Authorization -> Url

Like makeAuthorizationUrl, but gives you the ability to specify a custom response type and extra fields to be set on the query.

makeAuthorizationUrl : Authorization -> Url
makeAuthorizationUrl =
    makeAuthorizationUrlWith Code Dict.empty

For example, to interact with a service implementing OpenID+Connect you may require a different token type and an extra query parameter as such:

makeAuthorizationUrlWith
    (CustomResponse "code+id_token")
    (Dict.fromList [ ( "resource", "001" ) ])
    authorization


type AuthorizationResultWith error success
    = Empty
    | Error error
    | Success success

A parameterized AuthorizationResult, see parseTokenWith.

Authenticate

makeTokenRequestWith : OAuth.GrantType -> Json.Decode.Decoder success -> Dict String String -> (Result Http.Error success -> msg) -> Authentication -> RequestParts msg

Like makeTokenRequest, but gives you the ability to specify custom grant type and extra fields to be set on the query.

makeTokenRequest : (Result Http.Error AuthenticationSuccess -> msg) -> Authentication -> RequestParts msg
makeTokenRequest =
    makeTokenRequestWith
        AuthorizationCode
        defaultAuthenticationSuccessDecoder
        Dict.empty

Json Decoders

defaultExpiresInDecoder : Json.Decode.Decoder (Maybe Basics.Int)

Json decoder for the expiresIn field.

defaultScopeDecoder : Json.Decode.Decoder (List String)

Json decoder for the scope field (space-separated).

lenientScopeDecoder : Json.Decode.Decoder (List String)

Json decoder for the scope (comma- or space-separated).

defaultTokenDecoder : Json.Decode.Decoder OAuth.Token

Json decoder for the access_token field.

defaultRefreshTokenDecoder : Json.Decode.Decoder (Maybe OAuth.Token)

Json decoder for the refresh_token field.

defaultErrorDecoder : Json.Decode.Decoder OAuth.ErrorCode

Json decoder for the error field.

defaultErrorDescriptionDecoder : Json.Decode.Decoder (Maybe String)

Json decoder for the error_description field.

defaultErrorUriDecoder : Json.Decode.Decoder (Maybe String)

Json decoder for the error_uri field.

Query Parsers

parseCodeWith : Parsers error success -> Url -> AuthorizationResultWith error success

See parseCode, but gives you the ability to provide your own custom parsers.


type alias Parsers error success =
{ codeParser : Url.Parser.Query.Parser (Maybe String)
, errorParser : Url.Parser.Query.Parser (Maybe OAuth.ErrorCode)
, authorizationSuccessParser : String -> Url.Parser.Query.Parser success
, authorizationErrorParser : OAuth.ErrorCode -> Url.Parser.Query.Parser error 
}

Parsers used in the parseCode function.

defaultParsers : Parsers AuthorizationError AuthorizationSuccess

Default parsers according to RFC-6749.

defaultCodeParser : Url.Parser.Query.Parser (Maybe String)

Default code parser according to RFC-6749.

defaultErrorParser : Url.Parser.Query.Parser (Maybe OAuth.ErrorCode)

Default error parser according to RFC-6749.

defaultAuthorizationSuccessParser : String -> Url.Parser.Query.Parser AuthorizationSuccess

Default response success parser according to RFC-6749.

defaultAuthorizationErrorParser : OAuth.ErrorCode -> Url.Parser.Query.Parser AuthorizationError

Default response error parser according to RFC-6749.