yesod-auth-oauth2 0.7.1.0 → 0.7.1.1
raw patch · 31 files changed
+668/−593 lines, 31 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Network.OAuth.OAuth2.Compat: data OAuth2Error a
- Network.OAuth.OAuth2.Compat: type Errors = OAuth2Error Errors
+ Network.OAuth.OAuth2.Compat: type Errors = TokenRequestError
- Yesod.Auth.OAuth2.Prelude: throwIO :: (MonadThrow m, Exception e) => e -> m a
+ Yesod.Auth.OAuth2.Prelude: throwIO :: (HasCallStack, MonadThrow m, Exception e) => e -> m a
Files
- CHANGELOG.md +5/−1
- example/Main.hs +44/−40
- src/Network/OAuth/OAuth2/Compat.hs +33/−33
- src/URI/ByteString/Extension.hs +8/−4
- src/UnliftIO/Except.hs +3/−3
- src/Yesod/Auth/OAuth2.hs +11/−14
- src/Yesod/Auth/OAuth2/Auth0.hs +23/−20
- src/Yesod/Auth/OAuth2/AzureAD.hs +26/−23
- src/Yesod/Auth/OAuth2/AzureADv2.hs +38/−32
- src/Yesod/Auth/OAuth2/BattleNet.hs +24/−20
- src/Yesod/Auth/OAuth2/Bitbucket.hs +29/−26
- src/Yesod/Auth/OAuth2/ClassLink.hs +22/−19
- src/Yesod/Auth/OAuth2/Dispatch.hs +36/−29
- src/Yesod/Auth/OAuth2/DispatchError.hs +17/−18
- src/Yesod/Auth/OAuth2/ErrorResponse.hs +14/−15
- src/Yesod/Auth/OAuth2/EveOnline.hs +38/−34
- src/Yesod/Auth/OAuth2/Exception.hs +8/−11
- src/Yesod/Auth/OAuth2/GitHub.hs +23/−20
- src/Yesod/Auth/OAuth2/GitLab.hs +16/−14
- src/Yesod/Auth/OAuth2/Google.hs +27/−23
- src/Yesod/Auth/OAuth2/Nylas.hs +33/−29
- src/Yesod/Auth/OAuth2/Prelude.hs +16/−18
- src/Yesod/Auth/OAuth2/Random.hs +2/−2
- src/Yesod/Auth/OAuth2/Salesforce.hs +35/−28
- src/Yesod/Auth/OAuth2/Slack.hs +33/−25
- src/Yesod/Auth/OAuth2/Spotify.hs +23/−20
- src/Yesod/Auth/OAuth2/Twitch.hs +27/−24
- src/Yesod/Auth/OAuth2/Upcase.hs +21/−18
- src/Yesod/Auth/OAuth2/WordPressDotCom.hs +27/−23
- test/URI/ByteString/ExtensionSpec.hs +5/−6
- yesod-auth-oauth2.cabal +1/−1
CHANGELOG.md view
@@ -1,4 +1,8 @@-## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...main)+## [_Unreleased_](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.1...main)++## [v0.7.1.1](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.1.0...v0.7.1.1)++- Support `mtl-2.3`, which no longer re-exports `Control.Monad` ## [v0.7.1.0](https://github.com/thoughtbot/yesod-auth-oauth2/compare/v0.7.0.3...v0.7.1.0)
example/Main.hs view
@@ -13,7 +13,7 @@ import Data.ByteString.Lazy (fromStrict, toStrict) import qualified Data.Map as M import Data.Maybe (fromJust)-import Data.String (IsString(fromString))+import Data.String (IsString (fromString)) import Data.Text (Text, pack) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8)@@ -46,13 +46,15 @@ , appAuthPlugins :: [AuthPlugin App] } -mkYesod "App" [parseRoutes|+mkYesod+ "App"+ [parseRoutes| / RootR GET /auth AuthR Auth getAuth |] instance Yesod App where- -- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87+ -- see https://github.com/thoughtbot/yesod-auth-oauth2/issues/87 approot = ApprootStatic "http://localhost:3000" instance YesodAuth App where@@ -65,9 +67,9 @@ -- Copy the Creds response into the session for viewing after authenticate c = do- mapM_ (uncurry setSession)- $ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]- ++ credsExtra c+ mapM_ (uncurry setSession) $+ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]+ ++ credsExtra c return $ Authenticated "1" @@ -80,23 +82,24 @@ getRootR :: Handler Html getRootR = do- sess <- getSession+ sess <- getSession - let- prettify- = decodeUtf8- . toStrict- . encodePretty- . fromJust- . decode @Value- . fromStrict+ let+ prettify =+ decodeUtf8+ . toStrict+ . encodePretty+ . fromJust+ . decode @Value+ . fromStrict - mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess- mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess- mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess- mUserResponse = prettify <$> M.lookup "userResponse" sess+ mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess+ mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess+ mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess+ mUserResponse = prettify <$> M.lookup "userResponse" sess - defaultLayout [whamlet|+ defaultLayout+ [whamlet| <h1>Yesod Auth OAuth2 Example <h2> <a href=@{AuthR LoginR}>Log in@@ -123,32 +126,33 @@ azureTenant <- getEnv "AZURE_ADV2_TENANT_ID" appHttpManager <- newManager tlsManagerSettings- appAuthPlugins <- sequence+ appAuthPlugins <-+ sequence -- When Providers are added, add them here and update .env.example. -- Nothing else should need changing. -- -- FIXME: oauth2BattleNet is quite annoying! --- [ 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 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"+ ] - return App { .. }+ return App {..} where loadPlugin f prefix = do clientId <- getEnv $ prefix <> "_CLIENT_ID"
src/Network/OAuth/OAuth2/Compat.hs view
@@ -1,17 +1,17 @@ {-# LANGUAGE CPP #-} module Network.OAuth.OAuth2.Compat- ( OAuth2(..)- , OAuth2Result- , Errors- , authorizationUrl- , fetchAccessToken- , fetchAccessToken2- , authGetBS+ ( OAuth2 (..)+ , OAuth2Result+ , Errors+ , authorizationUrl+ , fetchAccessToken+ , fetchAccessToken2+ , authGetBS -- * Re-exports- , module Network.OAuth.OAuth2- ) where+ , module Network.OAuth.OAuth2+ ) where import Data.ByteString.Lazy (ByteString) import Data.Text (Text)@@ -39,12 +39,12 @@ #endif data OAuth2 = OAuth2- { oauth2ClientId :: Text- , oauth2ClientSecret :: Maybe Text- , oauth2AuthorizeEndpoint :: URIRef Absolute- , oauth2TokenEndpoint :: URIRef Absolute- , oauth2RedirectUri :: Maybe (URIRef Absolute)- }+ { oauth2ClientId :: Text+ , oauth2ClientSecret :: Maybe Text+ , oauth2AuthorizeEndpoint :: URIRef Absolute+ , oauth2TokenEndpoint :: URIRef Absolute+ , oauth2RedirectUri :: Maybe (URIRef Absolute)+ } #if MIN_VERSION_hoauth2(2,7,0) type Errors = TokenRequestError@@ -58,17 +58,17 @@ authorizationUrl = OAuth2.authorizationUrl . getOAuth2 fetchAccessToken- :: Manager- -> OAuth2- -> ExchangeToken- -> IO (OAuth2Result Errors OAuth2Token)+ :: Manager+ -> OAuth2+ -> ExchangeToken+ -> IO (OAuth2Result Errors OAuth2Token) fetchAccessToken = fetchAccessTokenBasic fetchAccessToken2- :: Manager- -> OAuth2- -> ExchangeToken- -> IO (OAuth2Result Errors OAuth2Token)+ :: Manager+ -> OAuth2+ -> ExchangeToken+ -> IO (OAuth2Result Errors OAuth2Token) fetchAccessToken2 = fetchAccessTokenPost authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)@@ -141,12 +141,12 @@ -- directly. fetchAccessTokenBasic- :: Manager- -> OAuth2- -> ExchangeToken- -> IO (OAuth2Result Errors OAuth2Token)+ :: Manager+ -> OAuth2+ -> ExchangeToken+ -> IO (OAuth2Result Errors OAuth2Token) fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e- where+ where #if MIN_VERSION_hoauth2(2,6,0) f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretBasic #elif MIN_VERSION_hoauth2(2,3,0)@@ -156,12 +156,12 @@ #endif fetchAccessTokenPost- :: Manager- -> OAuth2- -> ExchangeToken- -> IO (OAuth2Result Errors OAuth2Token)+ :: Manager+ -> OAuth2+ -> ExchangeToken+ -> IO (OAuth2Result Errors OAuth2Token) fetchAccessTokenPost m o e = runOAuth2 $ f m (getOAuth2 o) e- where+ where #if MIN_VERSION_hoauth2(2, 6, 0) f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretPost #elif MIN_VERSION_hoauth2(2,3,0)
src/URI/ByteString/Extension.hs view
@@ -1,9 +1,10 @@ {-# LANGUAGE FlexibleInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-}+ module URI.ByteString.Extension where import Data.ByteString (ByteString)-import Data.String (IsString(..))+import Data.String (IsString (..)) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Lens.Micro@@ -41,9 +42,12 @@ fromRelative s h = flip withHost h . toAbsolute s withHost :: URIRef a -> Host -> URIRef a-withHost u h = u & authorityL %~ maybe- (Just $ Authority Nothing h Nothing)- (\a -> Just $ a & authorityHostL .~ h)+withHost u h =+ u+ & authorityL+ %~ maybe+ (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,9 +1,9 @@ {-# OPTIONS_GHC -Wno-orphans #-} -module UnliftIO.Except- () where+module UnliftIO.Except () where -import Control.Monad.Except+import Control.Monad ((<=<))+import Control.Monad.Except (ExceptT (..), runExceptT) import UnliftIO instance (MonadUnliftIO m, Exception e) => MonadUnliftIO (ExceptT e m) where
src/Yesod/Auth/OAuth2.hs view
@@ -1,18 +1,18 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- Generic OAuth2 plugin for Yesod -- -- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage.--- module Yesod.Auth.OAuth2- ( OAuth2(..)+ ( OAuth2 (..) , FetchCreds , Manager- , OAuth2Token(..)- , Creds(..)+ , OAuth2Token (..)+ , Creds (..) , oauth2Url , authOAuth2 , authOAuth2Widget@@ -46,14 +46,12 @@ -- | Create an @'AuthPlugin'@ for the given OAuth2 provider -- -- Presents a generic @"Login via #{name}"@ link--- authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name -- | A version of 'authOAuth2' that uses 'fetchAccessToken2' -- -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>--- authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name @@ -61,7 +59,6 @@ -- -- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an -- example.--- authOAuth2Widget :: YesodAuth m => WidgetFor m ()@@ -74,7 +71,6 @@ -- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2' -- -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>--- authOAuth2Widget' :: YesodAuth m => WidgetFor m ()@@ -92,11 +88,13 @@ -> OAuth2 -> FetchCreds m -> AuthPlugin m-buildPlugin getToken widget name oauth getCreds = AuthPlugin- name- (dispatchAuthRequest name oauth getToken getCreds)- login- where login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]+buildPlugin getToken widget name oauth getCreds =+ AuthPlugin+ name+ (dispatchAuthRequest name oauth getToken getCreds)+ login+ where+ login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|] -- | Read the @'AccessToken'@ from the values set via @'setExtra'@ getAccessToken :: Creds m -> Maybe AccessToken@@ -105,7 +103,6 @@ -- | Read the @'RefreshToken'@ from the values set via @'setExtra'@ -- -- N.B. not all providers supply this value.--- getRefreshToken :: Creds m -> Maybe RefreshToken getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra
src/Yesod/Auth/OAuth2/Auth0.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- OAuth2 plugin for <https://auth0.com> -- -- * Authenticates against specific auth0 tenant -- * Uses Auth0 user id (a.k.a [sub](https://auth0.com/docs/api/authentication#get-user-info)) as credentials identifier--- module Yesod.Auth.OAuth2.Auth0 ( oauth2Auth0HostScopes , oauth2Auth0Host@@ -13,8 +13,8 @@ import Data.Aeson as Aeson import qualified Data.Text as T-import Prelude import Yesod.Auth.OAuth2.Prelude+import Prelude -- | https://auth0.com/docs/api/authentication#get-user-info newtype User = User T.Text@@ -36,22 +36,25 @@ :: 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- }+ oauth2 =+ OAuth2+ { 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
@@ -1,18 +1,18 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for Azure AD. -- -- * Authenticates against Azure AD -- * Uses email as credentials identifier--- module Yesod.Auth.OAuth2.AzureAD ( oauth2AzureAD , oauth2AzureADScoped ) where -import Prelude import Yesod.Auth.OAuth2.Prelude+import Prelude newtype User = User Text @@ -31,26 +31,29 @@ oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2AzureADScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://graph.microsoft.com/v1.0/me"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- , 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- }+ oauth2 =+ OAuth2+ { 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+ }
src/Yesod/Auth/OAuth2/AzureADv2.hs view
@@ -1,18 +1,18 @@ {-# 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 Prelude import Data.String import Data.Text (unpack)@@ -34,49 +34,55 @@ -- ^ Tenant Id -- -- If using a multi-tenant App, @common@ can be given here.- --- -> Text -- ^ Client Id- -> Text -- ^ Client secret+ -> Text+ -- ^ Client Id+ -> Text+ -- ^ Client secret -> AuthPlugin m oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes oauth2AzureADv2Scoped :: YesodAuth m- => [Text] -- ^ Scopes+ => [Text]+ -- ^ Scopes -> Text -- ^ Tenant Id -- -- If using a multi-tenant App, @common@ can be given here.- --- -> Text -- ^ Client Id- -> Text -- ^ Client Secret+ -> 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"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- , oauth2AuthorizeEndpoint =- tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = tenantUrl "/token"- , oauth2RedirectUri = Nothing- }+ 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+ fromString $+ "https://login.microsoftonline.com/"+ <> unpack tenantId+ <> "/oauth2/v2.0"+ <> path
src/Yesod/Auth/OAuth2/BattleNet.hs view
@@ -7,7 +7,6 @@ -- * Authenticates against battle.net. -- * Uses user's id as credentials identifier. -- * Returns user's battletag in extras.--- module Yesod.Auth.OAuth2.BattleNet ( oauth2BattleNet , oAuth2BattleNet@@ -28,32 +27,37 @@ oauth2BattleNet :: YesodAuth m- => WidgetFor m () -- ^ Login widget- -> Text -- ^ User region (e.g. "eu", "cn", "us")- -> Text -- ^ Client ID- -> Text -- ^ Client Secret+ => WidgetFor m ()+ -- ^ Login widget+ -> Text+ -- ^ User region (e.g. "eu", "cn", "us")+ -> Text+ -- ^ Client ID+ -> Text+ -- ^ Client Secret -> AuthPlugin m oauth2BattleNet widget region clientId clientSecret = authOAuth2Widget widget pluginName oauth2 $ \manager token -> do (User userId, userResponse) <-- authGetProfile pluginName manager token- $ fromRelative "https" (apiHost $ T.toLower region) "/account/user"+ 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- oauth2 = OAuth2- { oauth2ClientId = clientId- , oauth2ClientSecret = Just clientSecret- , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"- , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"- , oauth2RedirectUri = Nothing- }-+ oauth2 =+ OAuth2+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"+ , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"+ , oauth2RedirectUri = Nothing+ } apiHost :: Text -> Host apiHost "cn" = "api.battlenet.com.cn"
src/Yesod/Auth/OAuth2/Bitbucket.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://bitbucket.com -- -- * Authenticates against bitbucket -- * Uses bitbucket uuid as credentials identifier--- module Yesod.Auth.OAuth2.Bitbucket ( oauth2Bitbucket , oauth2BitbucketScoped@@ -32,30 +32,33 @@ oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2BitbucketScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://api.bitbucket.com/2.0/user"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ token+ "https://api.bitbucket.com/2.0/user" - 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- }+ 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+ } 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- }+ 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+ }
src/Yesod/Auth/OAuth2/ClassLink.hs view
@@ -26,24 +26,27 @@ oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2ClassLinkScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://nodeapi.classlink.com/v2/my/info"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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]- , oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"- , oauth2RedirectUri = Nothing- }+ oauth2 =+ OAuth2+ { 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+ }
src/Yesod/Auth/OAuth2/Dispatch.hs view
@@ -12,14 +12,15 @@ , dispatchAuthRequest ) where -import Control.Monad.Except+import Control.Monad (unless)+import Control.Monad.Except (MonadError (..)) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Network.HTTP.Conduit (Manager) import Network.OAuth.OAuth2.Compat-import UnliftIO.Exception import URI.ByteString.Extension+import UnliftIO.Exception import Yesod.Auth hiding (ServerError) import Yesod.Auth.OAuth2.DispatchError import Yesod.Auth.OAuth2.ErrorResponse@@ -29,21 +30,26 @@ -- | How to fetch an @'OAuth2Token'@ -- -- This will be 'fetchAccessToken' or 'fetchAccessToken2'----type FetchToken- = Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)+type FetchToken =+ Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token) -- | How to take an @'OAuth2Token'@ and retrieve user credentials type FetchCreds m = Manager -> OAuth2Token -> IO (Creds m) -- | Dispatch the various OAuth2 handshake routes dispatchAuthRequest- :: Text -- ^ Name- -> OAuth2 -- ^ Service details- -> FetchToken -- ^ How to get a token- -> FetchCreds m -- ^ How to get credentials- -> Text -- ^ Method- -> [Text] -- ^ Path pieces+ :: Text+ -- ^ Name+ -> OAuth2+ -- ^ Service details+ -> FetchToken+ -- ^ How to get a token+ -> FetchCreds m+ -- ^ How to get credentials+ -> Text+ -- ^ Method+ -> [Text]+ -- ^ Path pieces -> AuthHandler m TypedContent dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] = handleDispatchError $ dispatchForward name oauth2@@ -55,7 +61,6 @@ -- -- 1. Set a random CSRF token in our session -- 2. Redirect to the Provider's authorization URL--- dispatchForward :: (MonadError DispatchError m, MonadAuthHandler site m) => Text@@ -71,7 +76,6 @@ -- 1. Verify the URL's CSRF token matches our session -- 2. Use the code parameter to fetch an AccessToken for the Provider -- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider--- dispatchCallback :: (MonadError DispatchError m, MonadAuthHandler site m) => Text@@ -85,12 +89,13 @@ code <- requireGetParam "code" manager <- authHttpManager oauth2' <- withCallbackAndState name oauth2 csrf- token <- either (throwError . OAuth2ResultError) pure- =<< liftIO (getToken manager oauth2' $ ExchangeToken code)+ token <-+ either (throwError . OAuth2ResultError) pure+ =<< liftIO (getToken manager oauth2' $ ExchangeToken code) creds <- liftIO (getCreds manager token)- `catch` (throwError . FetchCredsIOException)- `catch` (throwError . FetchCredsYesodOAuth2Exception)+ `catch` (throwError . FetchCredsIOException)+ `catch` (throwError . FetchCredsYesodOAuth2Exception) setCredsRedirect creds withCallbackAndState@@ -102,29 +107,30 @@ withCallbackAndState name oauth2 csrf = do 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)]- }+ pure+ oauth2+ { oauth2RedirectUri = Just callback+ , oauth2AuthorizeEndpoint =+ oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]+ } getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text) getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent --- | Set a random, ~30-character value in the session+-- | Set a random, ~64-byte value in the session -- -- Some (but not all) providers decode a @+@ in the state token as a space when -- sending it back to us. We don't expect this and fail. And if we did code for -- it, we'd then fail on the providers that /don't/ do that. -- -- Therefore, we just exclude @+@ in our tokens, which means this function may--- return slightly less than 30 characters.---+-- return slightly fewer than 64 bytes. setSessionCSRF :: MonadHandler m => Text -> m Text setSessionCSRF sessionKey = do csrfToken <- liftIO randomToken csrfToken <$ setSession sessionKey csrfToken- where randomToken = T.filter (/= '+') <$> randomText 64+ where+ randomToken = T.filter (/= '+') <$> randomText 64 -- | Verify the callback provided the same CSRF token as in our session verifySessionCSRF@@ -133,9 +139,10 @@ 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
@@ -9,7 +9,7 @@ {-# LANGUAGE TypeFamilies #-} module Yesod.Auth.OAuth2.DispatchError- ( DispatchError(..)+ ( DispatchError (..) , handleDispatchError , onDispatchError ) where@@ -26,34 +26,33 @@ import Yesod.Core hiding (ErrorResponse) data DispatchError- = MissingParameter Text- | InvalidStateToken (Maybe Text) Text- | InvalidCallbackUri Text- | OAuth2HandshakeError ErrorResponse- | OAuth2ResultError Errors- | FetchCredsIOException IOException- | FetchCredsYesodOAuth2Exception YesodOAuth2Exception- | OtherDispatchError Text- deriving stock Show- deriving anyclass Exception+ = MissingParameter Text+ | InvalidStateToken (Maybe Text) Text+ | InvalidCallbackUri Text+ | OAuth2HandshakeError ErrorResponse+ | OAuth2ResultError Errors+ | FetchCredsIOException IOException+ | FetchCredsYesodOAuth2Exception YesodOAuth2Exception+ | OtherDispatchError Text+ deriving stock (Show)+ deriving anyclass (Exception) -- | User-friendly message for any given 'DispatchError' -- -- Most of these are opaque to the user. The exception details are present for -- the server logs.--- dispatchErrorMessage :: DispatchError -> Text dispatchErrorMessage = \case MissingParameter name -> "Parameter '" <> name <> "' is required, but not present in the URL"- InvalidStateToken{} -> "State token is invalid, please try again"- InvalidCallbackUri{} ->+ InvalidStateToken {} -> "State token is invalid, please try again"+ 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"- FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again"- OtherDispatchError{} -> "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" handleDispatchError :: MonadAuthHandler site m
src/Yesod/Auth/OAuth2/ErrorResponse.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | OAuth callback error response -- -- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>--- module Yesod.Auth.OAuth2.ErrorResponse- ( ErrorResponse(..)+ ( ErrorResponse (..) , erUserMessage- , ErrorName(..)+ , ErrorName (..) , onErrorResponse , unknownError ) where@@ -17,22 +17,22 @@ import Yesod.Core (MonadHandler, lookupGetParam) data ErrorName- = InvalidRequest- | UnauthorizedClient- | AccessDenied- | UnsupportedResponseType- | InvalidScope- | ServerError- | TemporarilyUnavailable- | Unknown Text- deriving Show+ = InvalidRequest+ | UnauthorizedClient+ | AccessDenied+ | UnsupportedResponseType+ | InvalidScope+ | ServerError+ | TemporarilyUnavailable+ | Unknown Text+ deriving (Show) data ErrorResponse = ErrorResponse { erName :: ErrorName , erDescription :: Maybe Text , erURI :: Maybe Text }- deriving Show+ deriving (Show) -- | Textual value suitable for display to a User erUserMessage :: ErrorResponse -> Text@@ -48,13 +48,12 @@ unknownError :: Text -> ErrorResponse unknownError x =- ErrorResponse { erName = Unknown x, erDescription = Nothing, erURI = Nothing }+ ErrorResponse {erName = Unknown x, erDescription = Nothing, erURI = Nothing} -- | Check query parameters for an error, if found run the given action -- -- The action is expected to use a short-circuit response function like -- @'permissionDenied'@, hence this returning @()@.--- onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m () onErrorResponse f = traverse_ f =<< checkErrorResponse
src/Yesod/Auth/OAuth2/EveOnline.hs view
@@ -1,16 +1,16 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- OAuth2 plugin for http://eveonline.com -- -- * Authenticates against eveonline -- * Uses EVEs unique account-user-char-hash as credentials identifier--- module Yesod.Auth.OAuth2.EveOnline ( oauth2Eve , oauth2EveScoped- , WidgetType(..)+ , WidgetType (..) ) where import Yesod.Auth.OAuth2.Prelude@@ -24,23 +24,24 @@ parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash" data WidgetType m- = Plain -- ^ Simple "Login via eveonline" text- | BigWhite- | SmallWhite- | BigBlack- | SmallBlack- | Custom (WidgetFor m ())+ = -- | Simple "Login via eveonline" text+ Plain+ | BigWhite+ | SmallWhite+ | BigBlack+ | SmallBlack+ | Custom (WidgetFor m ()) asWidget :: YesodAuth m => WidgetType m -> WidgetFor m () asWidget Plain = [whamlet|Login via eveonline|] asWidget BigWhite = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|]-asWidget BigBlack- = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]-asWidget SmallWhite- = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]-asWidget SmallBlack- = [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|]+asWidget BigBlack =+ [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]+asWidget SmallWhite =+ [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]+asWidget SmallBlack =+ [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|] asWidget (Custom a) = a pluginName :: Text@@ -57,25 +58,28 @@ oauth2EveScoped scopes widgetType clientId clientSecret = authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://login.eveonline.com/oauth/verify"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ token+ "https://login.eveonline.com/oauth/verify" - pure Creds- { credsPlugin = "eveonline"- -- FIXME: Preserved bug. See similar comment in Bitbucket provider.- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ pure+ Creds+ { credsPlugin = "eveonline"+ , -- FIXME: Preserved bug. See similar comment in Bitbucket provider.+ 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- }+ 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+ }
src/Yesod/Auth/OAuth2/Exception.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE DeriveDataTypeable #-} module Yesod.Auth.OAuth2.Exception- ( YesodOAuth2Exception(..)+ ( YesodOAuth2Exception (..) ) where import Control.Exception.Safe@@ -9,21 +9,18 @@ import Data.Text (Text) data YesodOAuth2Exception- = OAuth2Error Text ByteString- -- ^ HTTP error during OAuth2 handshake+ = -- | HTTP error during OAuth2 handshake -- -- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.- --- | JSONDecodingError Text String- -- ^ User profile was not as expected+ OAuth2Error Text ByteString+ | -- | User profile was not as expected -- -- Plugin name and Aeson parse error message.- --- | GenericError Text String- -- ^ Other error conditions+ JSONDecodingError Text String+ | -- | Other error conditions -- -- Plugin name and error message.- --- deriving (Show, Typeable)+ GenericError Text String+ deriving (Show, Typeable) instance Exception YesodOAuth2Exception
src/Yesod/Auth/OAuth2/GitHub.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://github.com -- -- * Authenticates against github -- * Uses github user id as credentials identifier--- module Yesod.Auth.OAuth2.GitHub ( oauth2GitHub , oauth2GitHubScoped@@ -32,24 +32,27 @@ oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2GitHubScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://api.github.com/user"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- }+ 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+ }
src/Yesod/Auth/OAuth2/GitLab.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+ module Yesod.Auth.OAuth2.GitLab ( oauth2GitLab , oauth2GitLabHostScopes@@ -32,7 +33,6 @@ -- -- > oauth2GitLabHostScopes defaultHost ["api", "read_user"] -- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes--- oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes @@ -43,17 +43,19 @@ (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- }+ oauth2 =+ OAuth2+ { 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
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ -- | -- -- OAuth2 plugin for http://www.google.com@@ -23,7 +24,6 @@ -- > updatedCreds = creds { credsIdent = email } -- > -- > -- continue normally with updatedCreds--- module Yesod.Auth.OAuth2.Google ( oauth2Google , oauth2GoogleWidget@@ -38,9 +38,10 @@ instance FromJSON User where parseJSON =- withObject "User" $ \o -> User- -- Required for data backwards-compatibility- <$> (("google-uid:" <>) <$> o .: "sub")+ withObject "User" $ \o ->+ User+ -- Required for data backwards-compatibility+ <$> (("google-uid:" <>) <$> o .: "sub") pluginName :: Text pluginName = "google"@@ -63,24 +64,27 @@ :: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m oauth2GoogleScopedWidget widget scopes clientId clientSecret = authOAuth2Widget widget pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://www.googleapis.com/oauth2/v3/userinfo"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- }+ 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+ }
src/Yesod/Auth/OAuth2/Nylas.hs view
@@ -26,41 +26,45 @@ oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Nylas clientId clientSecret = authOAuth2 pluginName oauth $ \manager token -> do- req <- applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""- <$> parseRequest "https://api.nylas.com/account"+ req <-+ applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""+ <$> parseRequest "https://api.nylas.com/account" resp <- httpLbs req manager let userResponse = responseBody resp -- 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: "- <> BL8.unpack userResponse+ unless (HT.statusIsSuccessful $ responseStatus resp) $+ 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- }- )+ (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)+ ( \(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)- -- 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- }+ oauth =+ OAuth2+ { 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+ }
src/Yesod/Auth/OAuth2/Prelude.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}+ -- | -- -- Modules and support functions required by most or all provider -- implementations. May also be useful for writing local providers.--- module Yesod.Auth.OAuth2.Prelude ( authGetProfile , scopeParam@@ -20,8 +20,8 @@ , (.:?) , (.=) , (<>)- , FromJSON(..)- , ToJSON(..)+ , FromJSON (..)+ , ToJSON (..) , eitherDecode , withObject @@ -29,22 +29,22 @@ , throwIO -- * OAuth2- , OAuth2(..)- , OAuth2Token(..)- , AccessToken(..)- , RefreshToken(..)+ , OAuth2 (..)+ , OAuth2Token (..)+ , AccessToken (..)+ , RefreshToken (..) -- * HTTP , Manager -- * Yesod- , YesodAuth(..)- , AuthPlugin(..)- , Creds(..)+ , YesodAuth (..)+ , AuthPlugin (..)+ , Creds (..) -- * Bytestring URI types , URI- , Host(..)+ , Host (..) -- * Bytestring URI extensions , module URI.ByteString.Extension@@ -74,7 +74,6 @@ -- The response should be parsed only far enough to read the required -- @'credsIdent'@. Additional information should either be re-parsed by or -- fetched via additional requests by consumers.--- authGetProfile :: FromJSON a => Text@@ -101,7 +100,7 @@ -- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter scopeParam :: Text -> [Text] -> (ByteString, ByteString)-scopeParam d = ("scope", ) . encodeUtf8 . T.intercalate d+scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d -- brittany-disable-next-binding @@ -115,10 +114,9 @@ -- May set the following keys: -- -- - @refreshToken@: if the provider supports refreshing the @accessToken@--- setExtra :: OAuth2Token -> BL.ByteString -> [(Text, Text)] setExtra token userResponse =- [ ("accessToken", atoken $ accessToken token)- , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)- ]- <> maybe [] (pure . ("refreshToken", ) . rtoken) (refreshToken token)+ [ ("accessToken", atoken $ accessToken token)+ , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)+ ]+ <> maybe [] (pure . ("refreshToken",) . rtoken) (refreshToken token)
src/Yesod/Auth/OAuth2/Random.hs view
@@ -5,7 +5,7 @@ ) where import Crypto.Random (MonadRandom, getRandomBytes)-import Data.ByteArray.Encoding (Base(Base64), convertToBase)+import Data.ByteArray.Encoding (Base (Base64), convertToBase) import Data.ByteString (ByteString) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8)@@ -13,7 +13,7 @@ randomText :: MonadRandom m => Int- -- ^ Size in Bytes (note necessarily characters)+ -- ^ Size in Bytes (not necessarily characters) -> m Text randomText size = decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size
src/Yesod/Auth/OAuth2/Salesforce.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://login.salesforce.com -- -- * Authenticates against Salesforce (or sandbox) -- * Uses Salesforce user id as credentials identifier--- module Yesod.Auth.OAuth2.Salesforce ( oauth2Salesforce , oauth2SalesforceScoped@@ -30,47 +30,54 @@ oauth2Salesforce = oauth2SalesforceScoped defaultScopes oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m-oauth2SalesforceScoped = salesforceHelper- pluginName- "https://login.salesforce.com/services/oauth2/userinfo"- "https://login.salesforce.com/services/oauth2/authorize"- "https://login.salesforce.com/services/oauth2/token"+oauth2SalesforceScoped =+ salesforceHelper+ pluginName+ "https://login.salesforce.com/services/oauth2/userinfo"+ "https://login.salesforce.com/services/oauth2/authorize"+ "https://login.salesforce.com/services/oauth2/token" oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes oauth2SalesforceSandboxScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m-oauth2SalesforceSandboxScoped = salesforceHelper- (pluginName <> "-sandbox")- "https://test.salesforce.com/services/oauth2/userinfo"- "https://test.salesforce.com/services/oauth2/authorize"- "https://test.salesforce.com/services/oauth2/token"+oauth2SalesforceSandboxScoped =+ salesforceHelper+ (pluginName <> "-sandbox")+ "https://test.salesforce.com/services/oauth2/userinfo"+ "https://test.salesforce.com/services/oauth2/authorize"+ "https://test.salesforce.com/services/oauth2/token" salesforceHelper :: YesodAuth m => Text- -> URI -- ^ User profile- -> URI -- ^ Authorize- -> URI -- ^ Token+ -> URI+ -- ^ User profile+ -> URI+ -- ^ Authorize+ -> URI+ -- ^ Token -> [Text] -> Text -> Text -> AuthPlugin m-salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret- = authOAuth2 name oauth2 $ \manager token -> do+salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret =+ 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- , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]- , oauth2TokenEndpoint = tokenUri- , oauth2RedirectUri = Nothing- }+ oauth2 =+ OAuth2+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]+ , oauth2TokenEndpoint = tokenUri+ , oauth2RedirectUri = Nothing+ }
src/Yesod/Auth/OAuth2/Slack.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- OAuth2 plugin for https://slack.com/ -- -- * Authenticates against slack -- * Uses slack user id as credentials identifier--- module Yesod.Auth.OAuth2.Slack- ( SlackScope(..)+ ( SlackScope (..) , oauth2Slack , oauth2SlackScoped ) where@@ -14,14 +14,18 @@ import Yesod.Auth.OAuth2.Prelude import Network.HTTP.Client- (httpLbs, parseUrlThrow, responseBody, setQueryString)+ ( httpLbs+ , parseUrlThrow+ , responseBody+ , setQueryString+ ) import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception data SlackScope- = SlackBasicScope- | SlackEmailScope- | SlackTeamScope- | SlackAvatarScope+ = SlackBasicScope+ | SlackEmailScope+ | SlackTeamScope+ | SlackAvatarScope scopeText :: SlackScope -> Text scopeText SlackBasicScope = "identity.basic"@@ -50,26 +54,30 @@ oauth2SlackScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do let param = encodeUtf8 $ atoken $ accessToken token- req <- setQueryString [("token", Just param)]- <$> parseUrlThrow "https://slack.com/api/users.identity"+ req <-+ setQueryString [("token", Just param)]+ <$> parseUrlThrow "https://slack.com/api/users.identity" userResponse <- responseBody <$> httpLbs req manager either- (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)- (\(User userId) -> pure Creds- { credsPlugin = pluginName- , credsIdent = userId- , credsExtra = setExtra token userResponse- }- )+ (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)+ ( \(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- }+ 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+ }
src/Yesod/Auth/OAuth2/Spotify.hs view
@@ -1,8 +1,8 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://spotify.com--- module Yesod.Auth.OAuth2.Spotify ( oauth2Spotify ) where@@ -20,24 +20,27 @@ oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2Spotify scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://api.spotify.com/v1/me"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- }+ 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+ }
src/Yesod/Auth/OAuth2/Twitch.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://twitch.tv -- -- * Authenticates against twitch -- * Uses twitch user id as credentials identifier--- module Yesod.Auth.OAuth2.Twitch ( oauth2Twitch , oauth2TwitchScoped@@ -32,28 +32,31 @@ oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2TwitchScoped scopes clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://id.twitch.tv/oauth2/validate"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- }+ 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+ }
src/Yesod/Auth/OAuth2/Upcase.hs view
@@ -1,11 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}+ -- | -- -- OAuth2 plugin for http://upcase.com -- -- * Authenticates against upcase -- * Uses upcase user id as credentials identifier--- module Yesod.Auth.OAuth2.Upcase ( oauth2Upcase ) where@@ -27,22 +27,25 @@ oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Upcase clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (User userId, userResponse) <- authGetProfile- pluginName- manager- token- "http://upcase.com/api/v1/me.json"+ (User userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ 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- , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"- , oauth2TokenEndpoint = "http://upcase.com/oauth/token"- , oauth2RedirectUri = Nothing- }+ oauth2 =+ OAuth2+ { oauth2ClientId = clientId+ , oauth2ClientSecret = Just clientSecret+ , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"+ , oauth2TokenEndpoint = "http://upcase.com/oauth/token"+ , oauth2RedirectUri = Nothing+ }
src/Yesod/Auth/OAuth2/WordPressDotCom.hs view
@@ -16,31 +16,35 @@ parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID" oauth2WordPressDotCom- :: (YesodAuth m)- => Text -- ^ Client Id- -> Text -- ^ Client Secret+ :: YesodAuth m+ => Text+ -- ^ Client Id+ -> Text+ -- ^ Client Secret -> AuthPlugin m oauth2WordPressDotCom clientId clientSecret = authOAuth2 pluginName oauth2 $ \manager token -> do- (WpUser userId, userResponse) <- authGetProfile- pluginName- manager- token- "https://public-api.wordpress.com/rest/v1/me/"-- pure Creds- { credsPlugin = pluginName- , credsIdent = T.pack $ show userId- , credsExtra = setExtra token userResponse- }+ (WpUser userId, userResponse) <-+ authGetProfile+ pluginName+ manager+ token+ "https://public-api.wordpress.com/rest/v1/me/" + pure+ Creds+ { credsPlugin = pluginName+ , credsIdent = T.pack $ show userId+ , credsExtra = setExtra token userResponse+ } where- oauth2 = OAuth2- { 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- }+ oauth2 =+ OAuth2+ { 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+ }
test/URI/ByteString/ExtensionSpec.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-}+ module URI.ByteString.ExtensionSpec ( spec ) where@@ -61,9 +62,8 @@ 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")]+ let uriWithQuery =+ [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")] uriWithQuery `shouldBe` [uri|http://example.com?foo=bar&baz=bat|] @@ -71,9 +71,8 @@ -- 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")]+ let uriWithQuery =+ [uri|http://example.com|] `withQuery` [("foo", "bar baz")] toText uriWithQuery `shouldBe` "http://example.com?foo=bar%20baz"
yesod-auth-oauth2.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: yesod-auth-oauth2-version: 0.7.1.0+version: 0.7.1.1 license: MIT license-file: LICENSE maintainer: engineering@freckle.com