truqu/elm-oauth2/3.0.0

NOTE: the latest version is 8.0.1

Elm OAuth 2

This package offers some utilities to implement a client-side OAuth 2 authorization in Elm. It covers all 4 grant types:

  • Implicit: The most commonly used. The token is obtained directly as a result of a user redirection to an OAuth provider.

  • Authorization Code: The token is obtained as a result of an authentication, from a code obtained as a result of a user redirection to an OAuth provider.

  • Resource Owner Password Credentials: The token is obtained directly by exchanging the user credentials with an OAuth provider.

  • Client Credentials: The token is obtained directly by exchanging application credentials with an OAuth provider.

Getting Started

The following parts is a walkthrough the first 2 flows. The last 2 are actually pretty straightforward and can be seen (in terms of steps) as a subset of the Authorization Code flow.

Installation

elm package install truqu/elm-oauth2

Usage (Implicit Flow)

A complete example is available here (with the corresponding sources here)

Imports
import OAuth
import OAuth.Implicit
Authorizing & Authenticating
type alias Model =
    { oauth :
        { clientId : String
        , redirectUri : Url
        }
    -- [ ... ]
    }


type Msg 
    = ClientIdSubmitted
    -- [ ... ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg ({ oauth } as model) =
    case msg of
        ClientIdSubmitted ->
            ( model
            , OAuth.Implicit.authorize
                { clientId = model.oauth.clientId
                , redirectUri = model.oauth.redirectUri
                , responseType = OAuth.Token
                , scope = [ "email", "profile" ]
                , state = Nothing
                , url = authorizationEndpoint
                }
            )

        -- [ ... ]
Parsing the token
init : () -> Url -> Key -> ( Model, Cmd Msg )
init _ origin navKey =
    let
        model =
            { oauth = { clientId = "", redirectUri = origin }
            , error = Nothing
            , token = Nothing
            , profile = Nothing
            }
    in
    case OAuth.Implicit.parse origin of
        Ok { token } ->
            ( { model | token = Just token }
            , getUserProfile profileEndpoint token
            )

        Err err ->
            ( { model | error = showParseErr err }
            , Cmd.none
            )
Using the token
getUserProfile : Url -> OAuth.Token -> Cmd Msg
getUserProfile endpoint token =
    Http.send GotUserInfo <|
        Http.request
            { method = "GET"
            , body = Http.emptyBody
            , headers = OAuth.use token []
            , withCredentials = False
            , url = Url.toString endpoint
            , expect = Http.expectJson profileDecoder
            , timeout = Nothing
            }

TroubleShooting

Interacting with GitHub

GitHub API v3 supports the Authorization Code Flow in order to obtain access tokens for a registered application. However, the implementation presents a flaw which makes it not compliant with the official RFC:

  • By default, GitHub's authorization server will respond with a x-www-form-urlencoded mime-type when trying to exchange the authorization code against an access token (instead of a application/json mime-type as specified in the official RFC).

  • This behavior can be changed by providing an extra Accept: application/json header with the authenticate request. However, by doing so, GitHub's authorization server will encode the scope of the response as a comma-separated list (instead of a space-separated list as specified in the official RFC).

Hence, this library provides a way to work around this implementation quirks by adjusting the authentication request before it gets sent. To achieve this, one may use the various decoders now exposed in OAuth.Decode to craft a custom transformation function for the authenticateWithOpts functions.

Here's a small example of how to work around GitHub's API v3 implementation:

adjustRequest : AdjustRequest ResponseToken
adjustRequest req =
    let
        headers =
            Http.header "Accept" ("application/json")  :: req.headers

        expect =
            Http.expectJson lenientResponseDecoder
    in
        { req | headers = headers, expect = expect }


getToken : String -> Cmd ResponseToken
getToken code =
    let
        req =
            OAuth.AuthorizationCode.authenticateWithOpts adjustRequest <|
                OAuth.AuthorizationCode
                    { credentials = { clientId = "clientId", secret = "secret" }
                    , code = code
                    , redirectUri = "redirectUri"
                    , scope = [ "whatever" ]
                    , state = Nothing
                    , url = "tokenEndpoint"
                    }
    in
        Http.send handleResponse req

Changelog

CHANGELOG.md