OAuth2 authentication for yesod
Go to file
patrick brisbin 8b3908ec91
Consolidate all errors, use onErrorHtml
Prior to this commit, some errors would be thrown (missing parameter,
invalid state, incorrect approot) while others would be handled via the
set-message-redirect approach (handshake failure, fetch-token failure,
etc).

This commit consolidates all of these cases into a single DispatchError
type, and then uses MonadError (concretely ExceptT) to capture them all
and handle them in one place ourselves.

It then updates that handling to:

- Use onErrorHtml

  onErrorHtml will, by default, set-message-redirect. That make this
  behavior neutral for users running defaults. For users that have
  customized this, it will be an improvement that all our error cases
  now respect it.

- Provided a JSON representation of errors
- Attach a random correlation identifier

The last two were just nice-to-haves that were cheap to add once the
code was in this state.

Note that the use of MonadError requires a potentially "bad" orphan
MonadUnliftIO instance for ExceptT, but I'd like to see that instance
become a reality and think it needs some real-world experimentation to
get there, so here I am.
2021-02-26 14:40:52 -05:00
.circleci Update nightly CI 2020-12-21 08:40:43 -05:00
example Add ClassLink plugin 2021-01-14 10:21:46 -05:00
src Consolidate all errors, use onErrorHtml 2021-02-26 14:40:52 -05:00
test Project setup files 2018-01-23 10:16:22 -05:00
.env.example Add ClassLink plugin 2021-01-14 10:21:46 -05:00
.gitignore Bring back example application 2018-02-13 08:59:01 -05:00
.hlint.yaml Address HLint issues 2018-01-23 10:16:22 -05:00
.stylish-haskell.yaml Project setup files 2018-01-23 10:16:22 -05:00
CHANGELOG.md Version bump 2021-02-03 11:58:31 -05:00
LICENSE Correct license information 2018-01-26 13:58:16 -05:00
Makefile Update nightly CI 2020-12-21 08:40:43 -05:00
package.yaml Consolidate all errors, use onErrorHtml 2021-02-26 14:40:52 -05:00
README.md Update to latest GHC, Stackage resolver, hoauth2 2020-08-24 10:49:14 -04:00
Setup.lhs Initial import 2013-07-14 11:11:44 +02:00
stack-lts-13.2.yaml Update to latest GHC, Stackage resolver, hoauth2 2020-08-24 10:49:14 -04:00
stack-lts-13.2.yaml.lock Update to latest GHC, Stackage resolver, hoauth2 2020-08-24 10:49:14 -04:00
stack-lts-16.10.yaml fixup! Update to latest GHC, Stackage resolver, hoauth2 2020-08-24 10:49:14 -04:00
stack-lts-16.10.yaml.lock fixup! Update to latest GHC, Stackage resolver, hoauth2 2020-08-24 10:49:14 -04:00
stack.yaml Bump LTS, bump dependencies upper-bounds 2020-12-21 08:40:43 -05:00
stack.yaml.lock Bump LTS, bump dependencies upper-bounds 2020-12-21 08:40:43 -05:00

Yesod.Auth.OAuth2

OAuth2 AuthPlugins for Yesod.

Usage

import Yesod.Auth
import Yesod.Auth.OAuth2.GitHub

instance YesodAuth App where
    -- ...

    authPlugins _ = [oauth2GitHub clientId clientSecret]

clientId :: Text
clientId = "..."

clientSecret :: Text
clientSecret = "..."

Some plugins, such as GitHub and Slack, have scoped functions for requesting additional information:

oauth2SlackScoped [SlackBasicScope, SlackEmailScope] clientId clientSecret

Working with Extra Data

We put the minimal amount of user data possible in credsExtra -- just enough to support you parsing or fetching additional data yourself.

For example, if you work with GitHub and GitHub user profiles, you likely already have a model and a way to parse the /user response. Rather than duplicate all that in our library, we try to make it easy for you to re-use that code yourself:

authenticate creds = do
    let
        -- You can run your own FromJSON parser on the response we already have
        eGitHubUser :: Either String GitHubUser
        eGitHubUser = getUserResponseJSON creds

        -- Avert your eyes, simplified example
        Just accessToken = getAccessToken creds
        Right githubUser = eGitHubUser

    -- Or make followup requests using our access token
    runGitHub accessToken $ userRepositories githubUser

    -- Or store it for later
    insert User
        { userIdent = credsIdent creds
        , userAccessToken = accessToken
        }

NOTE: Avoid looking up values in credsExtra yourself; prefer the provided get functions. The data representation itself is no longer considered public API.

Local Providers

If we don't supply a "Provider" (e.g. GitHub, Google, etc) you need, you can write your own using our provided Prelude:

import Yesod.Auth.OAuth2.Prelude

pluginName :: Text
pluginName = "mysite"

oauth2MySite :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2MySite clientId clientSecret =
    authOAuth2 pluginName oauth2 $ \manager token -> do
        -- Fetch a profile using the manager and token, leave it a ByteString
        userResponse <- -- ...

        -- Parse it to your preferred identifier, e.g. with Data.Aeson
        userId <- -- ...

        -- See authGetProfile for the typical case

        pure Creds
            { credsPlugin = pluginName
            , credsIdent = userId
            , credsExtra = setExtra token userResponse
            }
  where
    oauth2 = OAuth2
        { oauthClientId = clientId
        , oauthClientSecret = Just clientSecret
        , oauthOAuthorizeEndpoint = "https://mysite.com/oauth/authorize"
        , oauthAccessTokenEndpoint = "https://mysite.com/oauth/token"
        , oauthCallback = Nothing
        }

The Prelude module is considered public API, though we may build something higher-level that is more convenient for this use-case in the future.

Development & Tests

stack setup
stack build --dependencies-only
stack build --pedantic --test

Please also run HLint and Weeder before submitting PRs.


CHANGELOG | LICENSE