yesod-auth-oauth2 0.7.0.3 → 0.7.1.0
raw patch · 31 files changed
+448/−342 lines, 31 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Yesod.Auth.OAuth2.AzureADv2: instance Data.Aeson.Types.FromJSON.FromJSON Yesod.Auth.OAuth2.AzureADv2.User
+ Yesod.Auth.OAuth2.AzureADv2: oauth2AzureADv2 :: YesodAuth m => Text -> Text -> Text -> AuthPlugin m
+ Yesod.Auth.OAuth2.AzureADv2: oauth2AzureADv2Scoped :: YesodAuth m => [Text] -> Text -> Text -> Text -> AuthPlugin m
Files
- CHANGELOG.md +5/−1
- README.md +20/−1
- example/Main.hs +22/−32
- src/Network/OAuth/OAuth2/Compat.hs +1/−5
- src/URI/ByteString/Extension.hs +12/−16
- src/UnliftIO/Except.hs +4/−4
- src/Yesod/Auth/OAuth2/Auth0.hs +16/−15
- src/Yesod/Auth/OAuth2/AzureAD.hs +9/−8
- src/Yesod/Auth/OAuth2/AzureADv2.hs +82/−0
- src/Yesod/Auth/OAuth2/BattleNet.hs +12/−11
- src/Yesod/Auth/OAuth2/Bitbucket.hs +12/−10
- src/Yesod/Auth/OAuth2/ClassLink.hs +11/−9
- src/Yesod/Auth/OAuth2/Dispatch.hs +13/−12
- src/Yesod/Auth/OAuth2/DispatchError.hs +7/−6
- src/Yesod/Auth/OAuth2/ErrorResponse.hs +26/−30
- src/Yesod/Auth/OAuth2/EveOnline.hs +12/−12
- src/Yesod/Auth/OAuth2/Exception.hs +2/−2
- src/Yesod/Auth/OAuth2/GitHub.hs +12/−10
- src/Yesod/Auth/OAuth2/GitLab.hs +11/−11
- src/Yesod/Auth/OAuth2/Google.hs +12/−10
- src/Yesod/Auth/OAuth2/Nylas.hs +18/−18
- src/Yesod/Auth/OAuth2/Prelude.hs +2/−4
- src/Yesod/Auth/OAuth2/Random.hs +6/−6
- src/Yesod/Auth/OAuth2/Salesforce.hs +9/−8
- src/Yesod/Auth/OAuth2/Slack.hs +15/−15
- src/Yesod/Auth/OAuth2/Spotify.hs +12/−10
- src/Yesod/Auth/OAuth2/Twitch.hs +16/−13
- src/Yesod/Auth/OAuth2/Upcase.hs +9/−8
- src/Yesod/Auth/OAuth2/WordPressDotCom.hs +9/−8
- test/URI/ByteString/ExtensionSpec.hs +49/−46
- yesod-auth-oauth2.cabal +2/−1
CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.3...main)+## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...main)++## [v0.7.1.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.3...v0.7.1.0)++- Add `AzureADv2` provider ## [v0.7.0.3](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.2...v0.7.0.3)
README.md view
@@ -1,6 +1,5 @@ # Yesod.Auth.OAuth2 - [](https://hackage.haskell.org/package/yesod-auth-oauth2) [](http://stackage.org/nightly/package/yesod-auth-oauth2) [](http://stackage.org/lts/package/yesod-auth-oauth2)@@ -117,6 +116,26 @@ ``` Please also run HLint and Weeder before submitting PRs.++## Example++This project includes an executable that runs a server with (almost) all+supported providers present.++To use:++1. `cp .env.example .env` and edit in secrets for providers you wish to test++ Be sure to include `http://localhost:3000/auth/page/{plugin}/callback` as a+ valid Redirect URI when configuring the OAuth application.++2. Build with the example: `stack build ... --flag yesod-auth-oauth2:example`+3. Run the example `stack exec yesod-auth-oauth2-example`+4. Visit the example: `$BROWSER http://localhost:3000`+5. Click the log-in link for the provider you configured++If successful, you will be presented with a page that shows the credential and+User response value. ---
example/Main.hs view
@@ -6,19 +6,6 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} --- |------ This single-file Yesod app uses all plugins defined within this site, as a--- means of manual verification that they work. When adding a new plugin, add--- usage of it here and verify locally that it works.------ To do so, see @.env.example@, then:------ > stack build --flag yesod-auth-oauth2:example--- > stack exec yesod-auth-oauth2-example--- >--- > $BROWSER http://localhost:3000--- module Main where import Data.Aeson@@ -27,7 +14,7 @@ import qualified Data.Map as M import Data.Maybe (fromJust) import Data.String (IsString(fromString))-import Data.Text (Text)+import Data.Text (Text, pack) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8) import LoadEnv@@ -38,6 +25,7 @@ import Yesod.Auth import Yesod.Auth.OAuth2.Auth0 import Yesod.Auth.OAuth2.AzureAD+import Yesod.Auth.OAuth2.AzureADv2 import Yesod.Auth.OAuth2.BattleNet import Yesod.Auth.OAuth2.Bitbucket import Yesod.Auth.OAuth2.ClassLink@@ -78,7 +66,7 @@ -- Copy the Creds response into the session for viewing after authenticate c = do mapM_ (uncurry setSession)- $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]+ $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)] ++ credsExtra c return $ Authenticated "1"@@ -131,7 +119,8 @@ mkFoundation = do loadEnv - auth0Host <- getEnv "AUTH0_HOST"+ auth0Host <- getEnv "AUTH0_HOST"+ azureTenant <- getEnv "AZURE_ADV2_TENANT_ID" appHttpManager <- newManager tlsManagerSettings appAuthPlugins <- sequence@@ -140,28 +129,29 @@ -- -- FIXME: oauth2BattleNet is quite annoying! --- [ loadPlugin oauth2AzureAD "AZURE_AD"+ [ loadPlugin oauth2AzureAD "AZURE_AD"+ , loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2" , loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"- , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"- , loadPlugin oauth2Bitbucket "BITBUCKET"- , loadPlugin oauth2ClassLink "CLASSLINK"- , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"- , loadPlugin oauth2GitHub "GITHUB"- , loadPlugin oauth2GitLab "GITLAB"- , loadPlugin oauth2Google "GOOGLE"- , loadPlugin oauth2Nylas "NYLAS"- , loadPlugin oauth2Salesforce "SALES_FORCE"- , loadPlugin oauth2Slack "SLACK"- , loadPlugin (oauth2Spotify []) "SPOTIFY"- , loadPlugin oauth2Twitch "TWITCH"- , loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"- , loadPlugin oauth2Upcase "UPCASE"+ , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"+ , loadPlugin oauth2Bitbucket "BITBUCKET"+ , loadPlugin oauth2ClassLink "CLASSLINK"+ , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"+ , loadPlugin oauth2GitHub "GITHUB"+ , loadPlugin oauth2GitLab "GITLAB"+ , loadPlugin oauth2Google "GOOGLE"+ , loadPlugin oauth2Nylas "NYLAS"+ , loadPlugin oauth2Salesforce "SALES_FORCE"+ , loadPlugin oauth2Slack "SLACK"+ , loadPlugin (oauth2Spotify []) "SPOTIFY"+ , loadPlugin oauth2Twitch "TWITCH"+ , loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"+ , loadPlugin oauth2Upcase "UPCASE" ] return App { .. } where loadPlugin f prefix = do- clientId <- getEnv $ prefix <> "_CLIENT_ID"+ clientId <- getEnv $ prefix <> "_CLIENT_ID" clientSecret <- getEnv $ prefix <> "_CLIENT_SECRET" pure $ f (T.pack clientId) (T.pack clientSecret)
src/Network/OAuth/OAuth2/Compat.hs view
@@ -19,11 +19,7 @@ import qualified Network.OAuth.OAuth2 as OAuth2 #if MIN_VERSION_hoauth2(2,7,0) import Network.OAuth.OAuth2- ( AccessToken(..)- , ExchangeToken(..)- , OAuth2Token(..)- , RefreshToken(..)- )+ (AccessToken(..), ExchangeToken(..), OAuth2Token(..), RefreshToken(..)) import Network.OAuth.OAuth2.TokenRequest (TokenRequestError) #else import Network.OAuth.OAuth2
src/URI/ByteString/Extension.hs view
@@ -13,30 +13,26 @@ import URI.ByteString instance IsString Scheme where- fromString = Scheme . fromString+ fromString = Scheme . fromString instance IsString Host where- fromString = Host . fromString+ fromString = Host . fromString instance IsString (URIRef Absolute) where- fromString = either (error . show) id- . parseURI strictURIParserOptions- . C8.pack+ fromString =+ either (error . show) id . parseURI strictURIParserOptions . C8.pack instance IsString (URIRef Relative) where- fromString = either (error . show) id- . parseRelativeRef strictURIParserOptions- . C8.pack+ fromString =+ either (error . show) id . parseRelativeRef strictURIParserOptions . C8.pack fromText :: Text -> Maybe URI-fromText = either (const Nothing) Just- . parseURI strictURIParserOptions- . encodeUtf8+fromText =+ either (const Nothing) Just . parseURI strictURIParserOptions . encodeUtf8 unsafeFromText :: Text -> URI-unsafeFromText = either (error . show) id- . parseURI strictURIParserOptions- . encodeUtf8+unsafeFromText =+ either (error . show) id . parseURI strictURIParserOptions . encodeUtf8 toText :: URI -> Text toText = decodeUtf8 . serializeURIRef'@@ -46,8 +42,8 @@ withHost :: URIRef a -> Host -> URIRef a withHost u h = u & authorityL %~ maybe- (Just $ Authority Nothing h Nothing)- (\a -> Just $ a & authorityHostL .~ h)+ (Just $ Authority Nothing h Nothing)+ (\a -> Just $ a & authorityHostL .~ h) withPath :: URIRef a -> ByteString -> URIRef a withPath u p = u & pathL .~ p
src/UnliftIO/Except.hs view
@@ -1,12 +1,12 @@ {-# OPTIONS_GHC -Wno-orphans #-} module UnliftIO.Except- () where+ () where import Control.Monad.Except import UnliftIO instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where- withRunInIO exceptToIO = ExceptT $ try $ do- withRunInIO $ \runInIO ->- exceptToIO (runInIO . (either throwIO pure <=< runExceptT))+ withRunInIO exceptToIO = ExceptT $ try $ do+ withRunInIO $ \runInIO ->+ exceptToIO (runInIO . (either throwIO pure <=< runExceptT))
src/Yesod/Auth/OAuth2/Auth0.hs view
@@ -36,21 +36,22 @@ :: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m oauth2Auth0HostScopes host scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User uid, userResponse) <- authGetProfile pluginName- manager- token- (host `withPath` "/userinfo")- pure Creds { credsPlugin = pluginName- , credsIdent = uid- , credsExtra = setExtra token userResponse- }+ (User uid, userResponse) <- authGetProfile+ pluginName+ manager+ token+ (host `withPath` "/userinfo")+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = uid+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = host- `withPath` "/authorize"- `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = host `withPath` "/oauth/token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = host `withPath` "/oauth/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/AzureAD.hs view
@@ -37,19 +37,20 @@ token "https://graph.microsoft.com/v1.0/me" - pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret , oauth2AuthorizeEndpoint = "https://login.windows.net/common/oauth2/authorize" `withQuery` [ scopeParam "," scopes , ("resource", "https://graph.microsoft.com") ]- , oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"- , oauth2RedirectUri = Nothing+ , oauth2TokenEndpoint = "https://login.windows.net/common/oauth2/token"+ , oauth2RedirectUri = Nothing }
+ src/Yesod/Auth/OAuth2/AzureADv2.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+--+-- OAuth2 plugin for Azure AD using the new v2 endpoints.+--+-- * Authenticates against Azure AD+-- * Uses email as credentials identifier+--+module Yesod.Auth.OAuth2.AzureADv2+ ( oauth2AzureADv2+ , oauth2AzureADv2Scoped+ ) where++import Prelude+import Yesod.Auth.OAuth2.Prelude++import Data.String+import Data.Text (unpack)++newtype User = User Text++instance FromJSON User where+ parseJSON = withObject "User" $ \o -> User <$> o .: "mail"++pluginName :: Text+pluginName = "azureadv2"++defaultScopes :: [Text]+defaultScopes = ["openid", "profile"]++oauth2AzureADv2+ :: YesodAuth m+ => Text+ -- ^ Tenant Id+ --+ -- If using a multi-tenant App, @common@ can be given here.+ --+ -> Text -- ^ Client Id+ -> Text -- ^ Client secret+ -> AuthPlugin m+oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes++oauth2AzureADv2Scoped+ :: YesodAuth m+ => [Text] -- ^ Scopes+ -> Text+ -- ^ Tenant Id+ --+ -- If using a multi-tenant App, @common@ can be given here.+ --+ -> Text -- ^ Client Id+ -> Text -- ^ Client Secret+ -> AuthPlugin m+oauth2AzureADv2Scoped scopes tenantId clientId clientSecret =+ authOAuth2 pluginName oauth2 $ \manager token -> do+ (User userId, userResponse) <- authGetProfile+ pluginName+ manager+ token+ "https://graph.microsoft.com/v1.0/me"++ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ }+ where+ oauth2 = OAuth2+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = tenantUrl "/token"+ , oauth2RedirectUri = Nothing+ }++ tenantUrl path =+ fromString+ $ "https://login.microsoftonline.com/"+ <> unpack tenantId+ <> "/oauth2/v2.0"+ <> path
src/Yesod/Auth/OAuth2/BattleNet.hs view
@@ -39,27 +39,28 @@ authGetProfile pluginName manager token $ fromRelative "https" (apiHost $ T.toLower region) "/account/user" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where- host = wwwHost $ T.toLower region+ host = wwwHost $ T.toLower region oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"- , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"- , oauth2RedirectUri = Nothing+ , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"+ , oauth2RedirectUri = Nothing } apiHost :: Text -> Host-apiHost "cn" = "api.battlenet.com.cn"+apiHost "cn" = "api.battlenet.com.cn" apiHost region = Host $ encodeUtf8 $ region <> ".api.battle.net" wwwHost :: Text -> Host-wwwHost "cn" = "www.battlenet.com.cn"+wwwHost "cn" = "www.battlenet.com.cn" wwwHost region = Host $ encodeUtf8 $ region <> ".battle.net" oAuth2BattleNet
src/Yesod/Auth/OAuth2/Bitbucket.hs view
@@ -38,22 +38,24 @@ token "https://api.bitbucket.com/2.0/user" - pure Creds { credsPlugin = pluginName+ pure Creds+ { credsPlugin = pluginName -- FIXME: Preserved bug. This should just be userId (it's already -- a Text), but because this code was shipped, folks likely have -- Idents in their database like @"\"...\""@, and if we fixed this -- they would need migrating. We're keeping it for now as it's a -- minor wart. Breaking typed APIs is one thing, causing data to go -- invalid is another.- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://bitbucket.com/site/oauth2/authorize"- `withQuery` [scopeParam "," scopes]- , oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://bitbucket.com/site/oauth2/authorize"+ `withQuery` [scopeParam "," scopes]+ , oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/ClassLink.hs view
@@ -32,16 +32,18 @@ token "https://nodeapi.classlink.com/v2/my/info" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://launchpad.classlink.com/oauth2/v2/auth"- `withQuery` [scopeParam "," scopes]+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://launchpad.classlink.com/oauth2/v2/auth"+ `withQuery` [scopeParam "," scopes] , oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"- , oauth2RedirectUri = Nothing+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Dispatch.hs view
@@ -18,8 +18,8 @@ import Data.Text.Encoding (encodeUtf8) import Network.HTTP.Conduit (Manager) import Network.OAuth.OAuth2.Compat-import URI.ByteString.Extension import UnliftIO.Exception+import URI.ByteString.Extension import Yesod.Auth hiding (ServerError) import Yesod.Auth.OAuth2.DispatchError import Yesod.Auth.OAuth2.ErrorResponse@@ -62,7 +62,7 @@ -> OAuth2 -> m TypedContent dispatchForward name oauth2 = do- csrf <- setSessionCSRF $ tokenSessionKey name+ csrf <- setSessionCSRF $ tokenSessionKey name oauth2' <- withCallbackAndState name oauth2 csrf redirect $ toText $ authorizationUrl oauth2' @@ -81,11 +81,11 @@ -> m TypedContent dispatchCallback name oauth2 getToken getCreds = do onErrorResponse $ throwError . OAuth2HandshakeError- csrf <- verifySessionCSRF $ tokenSessionKey name- code <- requireGetParam "code"+ csrf <- verifySessionCSRF $ tokenSessionKey name+ code <- requireGetParam "code" manager <- authHttpManager oauth2' <- withCallbackAndState name oauth2 csrf- token <- either (throwError . OAuth2ResultError) pure+ token <- either (throwError . OAuth2ResultError) pure =<< liftIO (getToken manager oauth2' $ ExchangeToken code) creds <- liftIO (getCreds manager token)@@ -100,12 +100,12 @@ -> Text -> m OAuth2 withCallbackAndState name oauth2 csrf = do- uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender+ uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri pure oauth2- { oauth2RedirectUri = Just callback- , oauth2AuthorizeEndpoint = oauth2AuthorizeEndpoint oauth2- `withQuery` [("state", encodeUtf8 csrf)]+ { oauth2RedirectUri = Just callback+ , oauth2AuthorizeEndpoint =+ oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)] } getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text)@@ -130,11 +130,12 @@ verifySessionCSRF :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text verifySessionCSRF sessionKey = do- token <- requireGetParam "state"+ token <- requireGetParam "state" sessionToken <- lookupSession sessionKey deleteSession sessionKey- token <$ unless (sessionToken == Just token)- (throwError $ InvalidStateToken sessionToken token)+ token <$ unless+ (sessionToken == Just token)+ (throwError $ InvalidStateToken sessionToken token) requireGetParam :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text
src/Yesod/Auth/OAuth2/DispatchError.hs view
@@ -50,10 +50,10 @@ InvalidCallbackUri{} -> "Callback URI was not valid, this server may be misconfigured (no approot)" OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er- OAuth2ResultError{} -> "Login failed, please try again"- FetchCredsIOException{} -> "Login failed, please try again"+ OAuth2ResultError{} -> "Login failed, please try again"+ FetchCredsIOException{} -> "Login failed, please try again" FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again"- OtherDispatchError{} -> "Login failed, please try again"+ OtherDispatchError{} -> "Login failed, please try again" handleDispatchError :: MonadAuthHandler site m@@ -69,9 +69,10 @@ let suffix = " [errorId=" <> errorId <> "]" $(logError) $ pack (displayException err) <> suffix - let message = dispatchErrorMessage err <> suffix- messageValue =- object ["error" .= object ["id" .= errorId, "message" .= message]]+ let+ message = dispatchErrorMessage err <> suffix+ messageValue =+ object ["error" .= object ["id" .= errorId, "message" .= message]] loginR <- ($ LoginR) <$> getRouteToParent
src/Yesod/Auth/OAuth2/ErrorResponse.hs view
@@ -4,13 +4,12 @@ -- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1> -- module Yesod.Auth.OAuth2.ErrorResponse- ( ErrorResponse(..)- , erUserMessage- , ErrorName(..)- , onErrorResponse- , unknownError- )-where+ ( ErrorResponse(..)+ , erUserMessage+ , ErrorName(..)+ , onErrorResponse+ , unknownError+ ) where import Data.Foldable (traverse_) import Data.Text (Text)@@ -29,30 +28,27 @@ deriving Show data ErrorResponse = ErrorResponse- { erName :: ErrorName- , erDescription :: Maybe Text- , erURI :: Maybe Text- }- deriving Show+ { erName :: ErrorName+ , erDescription :: Maybe Text+ , erURI :: Maybe Text+ }+ deriving Show -- | Textual value suitable for display to a User erUserMessage :: ErrorResponse -> Text erUserMessage err = case erName err of- InvalidRequest -> "Invalid request"- UnauthorizedClient -> "Unauthorized client"- AccessDenied -> "Access denied"- UnsupportedResponseType -> "Unsupported response type"- InvalidScope -> "Invalid scope"- ServerError -> "Server error"- TemporarilyUnavailable -> "Temporarily unavailable"- Unknown _ -> "Unknown error"+ InvalidRequest -> "Invalid request"+ UnauthorizedClient -> "Unauthorized client"+ AccessDenied -> "Access denied"+ UnsupportedResponseType -> "Unsupported response type"+ InvalidScope -> "Invalid scope"+ ServerError -> "Server error"+ TemporarilyUnavailable -> "Temporarily unavailable"+ Unknown _ -> "Unknown error" unknownError :: Text -> ErrorResponse-unknownError x = ErrorResponse- { erName = Unknown x- , erDescription = Nothing- , erURI = Nothing- }+unknownError x =+ ErrorResponse { erName = Unknown x, erDescription = Nothing, erURI = Nothing } -- | Check query parameters for an error, if found run the given action --@@ -64,12 +60,12 @@ checkErrorResponse :: MonadHandler m => m (Maybe ErrorResponse) checkErrorResponse = do- merror <- lookupGetParam "error"+ merror <- lookupGetParam "error" - for merror $ \err ->- ErrorResponse (readErrorName err)- <$> lookupGetParam "error_description"- <*> lookupGetParam "error_uri"+ for merror $ \err ->+ ErrorResponse (readErrorName err)+ <$> lookupGetParam "error_description"+ <*> lookupGetParam "error_uri" readErrorName :: Text -> ErrorName readErrorName "invalid_request" = InvalidRequest
src/Yesod/Auth/OAuth2/EveOnline.hs view
@@ -63,19 +63,19 @@ token "https://login.eveonline.com/oauth/verify" - pure Creds { credsPlugin = "eveonline"+ pure Creds+ { credsPlugin = "eveonline" -- FIXME: Preserved bug. See similar comment in Bitbucket provider.- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://login.eveonline.com/oauth/authorize"- `withQuery` [ ("response_type", "code")- , scopeParam " " scopes- ]- , oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://login.eveonline.com/oauth/authorize"+ `withQuery` [("response_type", "code"), scopeParam " " scopes]+ , oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Exception.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE DeriveDataTypeable #-} module Yesod.Auth.OAuth2.Exception- ( YesodOAuth2Exception(..)- ) where+ ( YesodOAuth2Exception(..)+ ) where import Control.Exception.Safe import Data.ByteString.Lazy (ByteString)
src/Yesod/Auth/OAuth2/GitHub.hs view
@@ -38,16 +38,18 @@ token "https://api.github.com/user" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://github.com/login/oauth/authorize"- `withQuery` [scopeParam "," scopes]- , oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://github.com/login/oauth/authorize"+ `withQuery` [scopeParam "," scopes]+ , oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/GitLab.hs view
@@ -43,17 +43,17 @@ (User userId, userResponse) <- authGetProfile pluginName manager token $ host `withPath` "/api/v4/user" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = host- `withPath` "/oauth/authorize"- `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = host `withPath` "/oauth/token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = host `withPath` "/oauth/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Google.hs view
@@ -69,16 +69,18 @@ token "https://www.googleapis.com/oauth2/v3/userinfo" - pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"- `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://accounts.google.com/o/oauth2/auth"+ `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Nylas.hs view
@@ -34,33 +34,33 @@ -- FIXME: was this working? I'm 95% sure that the client will throw its -- own exception on unsuccessful status codes. unless (HT.statusIsSuccessful $ responseStatus resp)- $ throwIO- $ YesodOAuth2Exception.GenericError pluginName- $ "Unsuccessful HTTP response: "+ $ throwIO+ $ YesodOAuth2Exception.GenericError pluginName+ $ "Unsuccessful HTTP response: " <> BL8.unpack userResponse either (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)- (\(User userId) -> pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ (\(User userId) -> pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } ) $ eitherDecode userResponse where oauth = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://api.nylas.com/oauth/authorize"- `withQuery` [ ("response_type", "code")- , ( "client_id"- , encodeUtf8 clientId- )+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://api.nylas.com/oauth/authorize"+ `withQuery` [ ("response_type", "code")+ , ("client_id", encodeUtf8 clientId) -- N.B. The scopes delimeter is unknown/untested. Verify that before -- extracting this to an argument and offering a Scoped function. In -- its current state, it doesn't matter because it's only one scope.- , scopeParam "," defaultScopes- ]- , oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"- , oauth2RedirectUri = Nothing+ , scopeParam "," defaultScopes+ ]+ , oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Prelude.hs view
@@ -6,9 +6,7 @@ -- implementations. May also be useful for writing local providers. -- module Yesod.Auth.OAuth2.Prelude- (- -- * Provider helpers- authGetProfile+ ( authGetProfile , scopeParam , setExtra @@ -85,7 +83,7 @@ -> URI -> IO (a, BL.ByteString) authGetProfile name manager token url = do- resp <- fromAuthGet name =<< authGetBS manager (accessToken token) url+ resp <- fromAuthGet name =<< authGetBS manager (accessToken token) url decoded <- fromAuthJSON name resp pure (decoded, resp)
src/Yesod/Auth/OAuth2/Random.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE TypeApplications #-} module Yesod.Auth.OAuth2.Random- ( randomText- ) where+ ( randomText+ ) where import Crypto.Random (MonadRandom, getRandomBytes) import Data.ByteArray.Encoding (Base(Base64), convertToBase)@@ -11,9 +11,9 @@ import Data.Text.Encoding (decodeUtf8) randomText- :: MonadRandom m- => Int+ :: MonadRandom m+ => Int -- ^ Size in Bytes (note necessarily characters)- -> m Text+ -> m Text randomText size =- decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size+ decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size
src/Yesod/Auth/OAuth2/Salesforce.hs view
@@ -61,15 +61,16 @@ = authOAuth2 name oauth2 $ \manager token -> do (User userId, userResponse) <- authGetProfile name manager token profileUri - pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = tokenUri- , oauth2RedirectUri = Nothing+ , oauth2TokenEndpoint = tokenUri+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Slack.hs view
@@ -24,9 +24,9 @@ | SlackAvatarScope scopeText :: SlackScope -> Text-scopeText SlackBasicScope = "identity.basic"-scopeText SlackEmailScope = "identity.email"-scopeText SlackTeamScope = "identity.team"+scopeText SlackBasicScope = "identity.basic"+scopeText SlackEmailScope = "identity.email"+scopeText SlackTeamScope = "identity.team" scopeText SlackAvatarScope = "identity.avatar" newtype User = User Text@@ -56,20 +56,20 @@ either (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)- (\(User userId) -> pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ (\(User userId) -> pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } ) $ eitherDecode userResponse where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://slack.com/oauth/authorize"- `withQuery` [ scopeParam ","- $ map scopeText scopes- ]- , oauth2TokenEndpoint = "https://slack.com/api/oauth.access"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://slack.com/oauth/authorize"+ `withQuery` [scopeParam "," $ map scopeText scopes]+ , oauth2TokenEndpoint = "https://slack.com/api/oauth.access"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Spotify.hs view
@@ -26,16 +26,18 @@ token "https://api.spotify.com/v1/me" - pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://accounts.spotify.com/authorize"- `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://accounts.spotify.com/authorize"+ `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Twitch.hs view
@@ -38,19 +38,22 @@ token "https://id.twitch.tv/oauth2/validate" - pure Creds { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = "https://id.twitch.tv/oauth2/authorize"- `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = "https://id.twitch.tv/oauth2/token"- `withQuery` [ ("client_id", T.encodeUtf8 clientId)- , ("client_secret", T.encodeUtf8 clientSecret)- ]- , oauth2RedirectUri = Nothing+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint =+ "https://id.twitch.tv/oauth2/authorize"+ `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint =+ "https://id.twitch.tv/oauth2/token"+ `withQuery` [ ("client_id", T.encodeUtf8 clientId)+ , ("client_secret", T.encodeUtf8 clientSecret)+ ]+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/Upcase.hs view
@@ -33,15 +33,16 @@ token "http://upcase.com/api/v1/me.json" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"- , oauth2TokenEndpoint = "http://upcase.com/oauth/token"- , oauth2RedirectUri = Nothing+ , oauth2TokenEndpoint = "http://upcase.com/oauth/token"+ , oauth2RedirectUri = Nothing }
src/Yesod/Auth/OAuth2/WordPressDotCom.hs view
@@ -28,18 +28,19 @@ token "https://public-api.wordpress.com/rest/v1/me/" - pure Creds { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret , oauth2AuthorizeEndpoint = "https://public-api.wordpress.com/oauth2/authorize" `withQuery` [scopeParam "," ["auth"]]- , oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"- , oauth2RedirectUri = Nothing+ , oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"+ , oauth2RedirectUri = Nothing }
test/URI/ByteString/ExtensionSpec.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} module URI.ByteString.ExtensionSpec- ( spec- ) where+ ( spec+ ) where import Test.Hspec @@ -14,65 +14,68 @@ spec :: Spec spec = do- describe "IsString Scheme" $ it "works" $ do- "https" `shouldBe` Scheme "https"+ describe "IsString Scheme" $ it "works" $ do+ "https" `shouldBe` Scheme "https" - describe "IsString Host" $ it "works" $ do- "example.com" `shouldBe` Host "example.com"+ describe "IsString Host" $ it "works" $ do+ "example.com" `shouldBe` Host "example.com" - describe "IsString URIRef Relative" $ it "works" $ do- "example.com/foo?bar=baz"- `shouldBe` [relativeRef|example.com/foo?bar=baz|]+ describe "IsString URIRef Relative" $ it "works" $ do+ "example.com/foo?bar=baz" `shouldBe` [relativeRef|example.com/foo?bar=baz|] - describe "IsString URIRef Absolute" $ it "works" $ do- "https://example.com/foo?bar=baz"- `shouldBe` [uri|https://example.com/foo?bar=baz|]+ describe "IsString URIRef Absolute" $ it "works" $ do+ "https://example.com/foo?bar=baz"+ `shouldBe` [uri|https://example.com/foo?bar=baz|] - describe "fromText" $ do- it "returns Just a URI for valid values, as the quasi-quoter would" $ do- fromText "http://example.com/foo?bar=baz"- `shouldBe` Just [uri|http://example.com/foo?bar=baz|]+ describe "fromText" $ do+ it "returns Just a URI for valid values, as the quasi-quoter would" $ do+ fromText "http://example.com/foo?bar=baz"+ `shouldBe` Just [uri|http://example.com/foo?bar=baz|] - it "returns Nothing for invalid values" $ do- fromText "Oh my, what did I do?" `shouldBe` Nothing+ it "returns Nothing for invalid values" $ do+ fromText "Oh my, what did I do?" `shouldBe` Nothing - describe "unsafeFromText" $ do- it "returns a URI for valid values, as the quasi-quoter would" $ do- unsafeFromText "http://example.com/foo?bar=baz"- `shouldBe` [uri|http://example.com/foo?bar=baz|]+ describe "unsafeFromText" $ do+ it "returns a URI for valid values, as the quasi-quoter would" $ do+ unsafeFromText "http://example.com/foo?bar=baz"+ `shouldBe` [uri|http://example.com/foo?bar=baz|] - it "raises for invalid values" $ do- evaluate (unsafeFromText "Oh my, what did I do?")- `shouldThrow` errorContaining "MissingColon"+ it "raises for invalid values" $ do+ evaluate (unsafeFromText "Oh my, what did I do?")+ `shouldThrow` errorContaining "MissingColon" - describe "toText" $ do- it "serializes the URI to text" $ do- toText [uri|https://example.com/foo?bar=baz|]- `shouldBe` "https://example.com/foo?bar=baz"+ describe "toText" $ do+ it "serializes the URI to text" $ do+ toText [uri|https://example.com/foo?bar=baz|]+ `shouldBe` "https://example.com/foo?bar=baz" - describe "fromRelative" $ do- it "makes a URI absolute with a given host" $ do- fromRelative "ftp" "foo.com" [relativeRef|/bar?baz=bat|]- `shouldBe` [uri|ftp://foo.com/bar?baz=bat|]+ describe "fromRelative" $ do+ it "makes a URI absolute with a given host" $ do+ fromRelative "ftp" "foo.com" [relativeRef|/bar?baz=bat|]+ `shouldBe` [uri|ftp://foo.com/bar?baz=bat|] - describe "withQuery" $ do- it "appends a query to a URI" $ do- let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar")]+ describe "withQuery" $ do+ it "appends a query to a URI" $ do+ let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar")] - uriWithQuery `shouldBe` [uri|http://example.com?foo=bar|]+ uriWithQuery `shouldBe` [uri|http://example.com?foo=bar|] - it "handles a URI with an existing query" $ do- let uriWithQuery = [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]+ it "handles a URI with an existing query" $ do+ let+ uriWithQuery =+ [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")] - uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|]+ uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|] - -- This is arguably testing the internals of another package, but IMO- -- it's worthwhile to show that you don't (and can't) pre-sanitize when- -- using this function.- it "handles santization of the query" $ do- let uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar baz")]+ -- This is arguably testing the internals of another package, but IMO+ -- it's worthwhile to show that you don't (and can't) pre-sanitize when+ -- using this function.+ it "handles santization of the query" $ do+ let+ uriWithQuery =+ [uri|http://example.com|] `withQuery` [("foo", "bar baz")] - toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz"+ toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz" errorContaining :: String -> Selector ErrorCall errorContaining msg = (msg `isInfixOf`) . show
yesod-auth-oauth2.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: yesod-auth-oauth2-version: 0.7.0.3+version: 0.7.1.0 license: MIT license-file: LICENSE maintainer: engineering@freckle.com@@ -37,6 +37,7 @@ Yesod.Auth.OAuth2 Yesod.Auth.OAuth2.Auth0 Yesod.Auth.OAuth2.AzureAD+ Yesod.Auth.OAuth2.AzureADv2 Yesod.Auth.OAuth2.BattleNet Yesod.Auth.OAuth2.Bitbucket Yesod.Auth.OAuth2.ClassLink