Convert project to Fourmolu

This commit is contained in:
patrick brisbin 2023-08-01 07:59:37 -04:00 committed by Pat Brisbin
parent 5d4e4f8d7b
commit 08d0f0eaa4
33 changed files with 673 additions and 660 deletions

View File

@ -1,10 +1,4 @@
restylers: restylers:
- brittany: - fourmolu
include: - "!stylish-haskell"
- "**/*.hs"
- "!src/Network/OAuth/OAuth2/Compat.hs" # CPP
- stylish-haskell:
include:
- "**/*.hs"
- "!src/Network/OAuth/OAuth2/Compat.hs" # CPP
- "*" - "*"

View File

@ -1,21 +0,0 @@
steps:
- simple_align:
cases: false
top_level_patterns: false
records: false
- imports:
align: none
list_align: after_alias
pad_module_names: false
long_list_align: new_line_multiline
empty_list_align: right_after
list_padding: 4
separate_lists: false
space_surround: false
- language_pragmas:
style: vertical
align: false
remove_redundant: true
- trailing_whitespace: {}
columns: 80
newline: native

View File

@ -1,44 +0,0 @@
---
conf_debug:
dconf_roundtrip_exactprint_only: false
dconf_dump_bridoc_simpl_par: false
dconf_dump_ast_unknown: false
dconf_dump_bridoc_simpl_floating: false
dconf_dump_config: false
dconf_dump_bridoc_raw: false
dconf_dump_bridoc_final: false
dconf_dump_bridoc_simpl_alt: false
dconf_dump_bridoc_simpl_indent: false
dconf_dump_annotations: false
dconf_dump_bridoc_simpl_columns: false
dconf_dump_ast_full: false
conf_errorHandling:
econf_ExactPrintFallback: ExactPrintFallbackModeInline
econf_Werror: false
econf_omit_output_valid_check: false
econf_produceOutputOnErrors: false
conf_preprocessor:
ppconf_CPPMode: CPPModeAbort
ppconf_hackAroundIncludes: false
conf_obfuscate: false
conf_roundtrip_exactprint_only: false
conf_version: 1
conf_layout:
lconfig_reformatModulePreamble: true
lconfig_altChooser:
tag: AltChooserBoundedSearch
contents: 3
lconfig_allowSingleLineExportList: false
lconfig_importColumn: 60
lconfig_hangingTypeSignature: false
lconfig_importAsColumn: 50
lconfig_alignmentLimit: 1
lconfig_indentListSpecial: true
lconfig_indentAmount: 2
lconfig_alignmentBreakOnMultiline: true
lconfig_cols: 80
lconfig_indentPolicy: IndentPolicyLeft
lconfig_indentWhereSpecial: true
lconfig_columnAlignMode:
tag: ColumnAlignModeDisabled
contents: 0.7

View File

@ -13,7 +13,7 @@ import Data.Aeson.Encode.Pretty
import Data.ByteString.Lazy (fromStrict, toStrict) import Data.ByteString.Lazy (fromStrict, toStrict)
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString(fromString)) import Data.String (IsString (fromString))
import Data.Text (Text, pack) import Data.Text (Text, pack)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
@ -46,13 +46,15 @@ data App = App
, appAuthPlugins :: [AuthPlugin App] , appAuthPlugins :: [AuthPlugin App]
} }
mkYesod "App" [parseRoutes| mkYesod
"App"
[parseRoutes|
/ RootR GET / RootR GET
/auth AuthR Auth getAuth /auth AuthR Auth getAuth
|] |]
instance Yesod App where 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" approot = ApprootStatic "http://localhost:3000"
instance YesodAuth App where instance YesodAuth App where
@ -65,9 +67,9 @@ instance YesodAuth App where
-- Copy the Creds response into the session for viewing after -- Copy the Creds response into the session for viewing after
authenticate c = do authenticate c = do
mapM_ (uncurry setSession) mapM_ (uncurry setSession) $
$ [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)] [("credsIdent", credsIdent c), ("credsPlugin", credsPlugin c)]
++ credsExtra c ++ credsExtra c
return $ Authenticated "1" return $ Authenticated "1"
@ -80,23 +82,24 @@ instance RenderMessage App FormMessage where
getRootR :: Handler Html getRootR :: Handler Html
getRootR = do getRootR = do
sess <- getSession sess <- getSession
let let
prettify prettify =
= decodeUtf8 decodeUtf8
. toStrict . toStrict
. encodePretty . encodePretty
. fromJust . fromJust
. decode @Value . decode @Value
. fromStrict . fromStrict
mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess mCredsIdent = decodeUtf8 <$> M.lookup "credsIdent" sess
mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess mCredsPlugin = decodeUtf8 <$> M.lookup "credsPlugin" sess
mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess mAccessToken = decodeUtf8 <$> M.lookup "accessToken" sess
mUserResponse = prettify <$> M.lookup "userResponse" sess mUserResponse = prettify <$> M.lookup "userResponse" sess
defaultLayout [whamlet| defaultLayout
[whamlet|
<h1>Yesod Auth OAuth2 Example <h1>Yesod Auth OAuth2 Example
<h2> <h2>
<a href=@{AuthR LoginR}>Log in <a href=@{AuthR LoginR}>Log in
@ -123,32 +126,33 @@ mkFoundation = do
azureTenant <- getEnv "AZURE_ADV2_TENANT_ID" azureTenant <- getEnv "AZURE_ADV2_TENANT_ID"
appHttpManager <- newManager tlsManagerSettings appHttpManager <- newManager tlsManagerSettings
appAuthPlugins <- sequence appAuthPlugins <-
sequence
-- When Providers are added, add them here and update .env.example. -- When Providers are added, add them here and update .env.example.
-- Nothing else should need changing. -- Nothing else should need changing.
-- --
-- FIXME: oauth2BattleNet is quite annoying! -- FIXME: oauth2BattleNet is quite annoying!
-- --
[ loadPlugin oauth2AzureAD "AZURE_AD" [ loadPlugin oauth2AzureAD "AZURE_AD"
, loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2" , loadPlugin (oauth2AzureADv2 $ pack azureTenant) "AZURE_ADV2"
, loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0" , loadPlugin (oauth2Auth0Host $ fromString auth0Host) "AUTH0"
, loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET" , loadPlugin (oauth2BattleNet [whamlet|TODO|] "en") "BATTLE_NET"
, loadPlugin oauth2Bitbucket "BITBUCKET" , loadPlugin oauth2Bitbucket "BITBUCKET"
, loadPlugin oauth2ClassLink "CLASSLINK" , loadPlugin oauth2ClassLink "CLASSLINK"
, loadPlugin (oauth2Eve Plain) "EVE_ONLINE" , loadPlugin (oauth2Eve Plain) "EVE_ONLINE"
, loadPlugin oauth2GitHub "GITHUB" , loadPlugin oauth2GitHub "GITHUB"
, loadPlugin oauth2GitLab "GITLAB" , loadPlugin oauth2GitLab "GITLAB"
, loadPlugin oauth2Google "GOOGLE" , loadPlugin oauth2Google "GOOGLE"
, loadPlugin oauth2Nylas "NYLAS" , loadPlugin oauth2Nylas "NYLAS"
, loadPlugin oauth2Salesforce "SALES_FORCE" , loadPlugin oauth2Salesforce "SALES_FORCE"
, loadPlugin oauth2Slack "SLACK" , loadPlugin oauth2Slack "SLACK"
, loadPlugin (oauth2Spotify []) "SPOTIFY" , loadPlugin (oauth2Spotify []) "SPOTIFY"
, loadPlugin oauth2Twitch "TWITCH" , loadPlugin oauth2Twitch "TWITCH"
, loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM" , loadPlugin oauth2WordPressDotCom "WORDPRESS_DOT_COM"
, loadPlugin oauth2Upcase "UPCASE" , loadPlugin oauth2Upcase "UPCASE"
] ]
return App { .. } return App {..}
where where
loadPlugin f prefix = do loadPlugin f prefix = do
clientId <- getEnv $ prefix <> "_CLIENT_ID" clientId <- getEnv $ prefix <> "_CLIENT_ID"

15
fourmolu.yaml Normal file
View File

@ -0,0 +1,15 @@
indentation: 2
column-limit: 80 # ignored until v12 / ghc-9.6
function-arrows: leading
comma-style: leading # default
import-export-style: leading
indent-wheres: false # default
record-brace-space: true
newlines-between-decls: 1 # default
haddock-style: single-line
let-style: mixed
in-style: left-align
single-constraint-parens: never # ignored until v12 / ghc-9.6
unicode: never # default
respectful: true # default
fixities: [] # default

View File

@ -1,17 +1,17 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Network.OAuth.OAuth2.Compat module Network.OAuth.OAuth2.Compat
( OAuth2(..) ( OAuth2 (..)
, OAuth2Result , OAuth2Result
, Errors , Errors
, authorizationUrl , authorizationUrl
, fetchAccessToken , fetchAccessToken
, fetchAccessToken2 , fetchAccessToken2
, authGetBS , authGetBS
-- * Re-exports -- * Re-exports
, module Network.OAuth.OAuth2 , module Network.OAuth.OAuth2
) where ) where
import Data.ByteString.Lazy (ByteString) import Data.ByteString.Lazy (ByteString)
import Data.Text (Text) import Data.Text (Text)
@ -39,12 +39,12 @@ import Data.Maybe (fromMaybe)
#endif #endif
data OAuth2 = OAuth2 data OAuth2 = OAuth2
{ oauth2ClientId :: Text { oauth2ClientId :: Text
, oauth2ClientSecret :: Maybe Text , oauth2ClientSecret :: Maybe Text
, oauth2AuthorizeEndpoint :: URIRef Absolute , oauth2AuthorizeEndpoint :: URIRef Absolute
, oauth2TokenEndpoint :: URIRef Absolute , oauth2TokenEndpoint :: URIRef Absolute
, oauth2RedirectUri :: Maybe (URIRef Absolute) , oauth2RedirectUri :: Maybe (URIRef Absolute)
} }
#if MIN_VERSION_hoauth2(2,7,0) #if MIN_VERSION_hoauth2(2,7,0)
type Errors = TokenRequestError type Errors = TokenRequestError
@ -58,17 +58,17 @@ authorizationUrl :: OAuth2 -> URI
authorizationUrl = OAuth2.authorizationUrl . getOAuth2 authorizationUrl = OAuth2.authorizationUrl . getOAuth2
fetchAccessToken fetchAccessToken
:: Manager :: Manager
-> OAuth2 -> OAuth2
-> ExchangeToken -> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token) -> IO (OAuth2Result Errors OAuth2Token)
fetchAccessToken = fetchAccessTokenBasic fetchAccessToken = fetchAccessTokenBasic
fetchAccessToken2 fetchAccessToken2
:: Manager :: Manager
-> OAuth2 -> OAuth2
-> ExchangeToken -> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token) -> IO (OAuth2Result Errors OAuth2Token)
fetchAccessToken2 = fetchAccessTokenPost fetchAccessToken2 = fetchAccessTokenPost
authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString) authGetBS :: Manager -> AccessToken -> URI -> IO (Either ByteString ByteString)
@ -141,12 +141,12 @@ runOAuth2 = id
-- directly. -- directly.
fetchAccessTokenBasic fetchAccessTokenBasic
:: Manager :: Manager
-> OAuth2 -> OAuth2
-> ExchangeToken -> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token) -> IO (OAuth2Result Errors OAuth2Token)
fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e
where where
#if MIN_VERSION_hoauth2(2,6,0) #if MIN_VERSION_hoauth2(2,6,0)
f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretBasic f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretBasic
#elif MIN_VERSION_hoauth2(2,3,0) #elif MIN_VERSION_hoauth2(2,3,0)
@ -156,12 +156,12 @@ fetchAccessTokenBasic m o e = runOAuth2 $ f m (getOAuth2 o) e
#endif #endif
fetchAccessTokenPost fetchAccessTokenPost
:: Manager :: Manager
-> OAuth2 -> OAuth2
-> ExchangeToken -> ExchangeToken
-> IO (OAuth2Result Errors OAuth2Token) -> IO (OAuth2Result Errors OAuth2Token)
fetchAccessTokenPost m o e = runOAuth2 $ f m (getOAuth2 o) e fetchAccessTokenPost m o e = runOAuth2 $ f m (getOAuth2 o) e
where where
#if MIN_VERSION_hoauth2(2, 6, 0) #if MIN_VERSION_hoauth2(2, 6, 0)
f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretPost f = OAuth2.fetchAccessTokenWithAuthMethod OAuth2.ClientSecretPost
#elif MIN_VERSION_hoauth2(2,3,0) #elif MIN_VERSION_hoauth2(2,3,0)

View File

@ -1,9 +1,10 @@
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module URI.ByteString.Extension where module URI.ByteString.Extension where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.String (IsString(..)) import Data.String (IsString (..))
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Lens.Micro import Lens.Micro
@ -41,9 +42,12 @@ fromRelative :: Scheme -> Host -> RelativeRef -> URI
fromRelative s h = flip withHost h . toAbsolute s fromRelative s h = flip withHost h . toAbsolute s
withHost :: URIRef a -> Host -> URIRef a withHost :: URIRef a -> Host -> URIRef a
withHost u h = u & authorityL %~ maybe withHost u h =
(Just $ Authority Nothing h Nothing) u
(\a -> Just $ a & authorityHostL .~ h) & authorityL
%~ maybe
(Just $ Authority Nothing h Nothing)
(\a -> Just $ a & authorityHostL .~ h)
withPath :: URIRef a -> ByteString -> URIRef a withPath :: URIRef a -> ByteString -> URIRef a
withPath u p = u & pathL .~ p withPath u p = u & pathL .~ p

View File

@ -1,7 +1,6 @@
{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-orphans #-}
module UnliftIO.Except module UnliftIO.Except () where
() where
import Control.Monad.Except import Control.Monad.Except
import UnliftIO import UnliftIO

View File

@ -1,18 +1,18 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- Generic OAuth2 plugin for Yesod -- Generic OAuth2 plugin for Yesod
-- --
-- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage. -- See @"Yesod.Auth.OAuth2.GitHub"@ for example usage.
--
module Yesod.Auth.OAuth2 module Yesod.Auth.OAuth2
( OAuth2(..) ( OAuth2 (..)
, FetchCreds , FetchCreds
, Manager , Manager
, OAuth2Token(..) , OAuth2Token (..)
, Creds(..) , Creds (..)
, oauth2Url , oauth2Url
, authOAuth2 , authOAuth2
, authOAuth2Widget , authOAuth2Widget
@ -46,14 +46,12 @@ oauth2Url name = PluginR name ["forward"]
-- | Create an @'AuthPlugin'@ for the given OAuth2 provider -- | Create an @'AuthPlugin'@ for the given OAuth2 provider
-- --
-- Presents a generic @"Login via #{name}"@ link -- Presents a generic @"Login via #{name}"@ link
--
authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2 :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m
authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name authOAuth2 name = authOAuth2Widget [whamlet|Login via #{name}|] name
-- | A version of 'authOAuth2' that uses 'fetchAccessToken2' -- | A version of 'authOAuth2' that uses 'fetchAccessToken2'
-- --
-- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129> -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>
--
authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m authOAuth2' :: YesodAuth m => Text -> OAuth2 -> FetchCreds m -> AuthPlugin m
authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name
@ -61,7 +59,6 @@ authOAuth2' name = authOAuth2Widget' [whamlet|Login via #{name}|] name
-- --
-- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an -- Allows passing a custom widget for the login link. See @'oauth2Eve'@ for an
-- example. -- example.
--
authOAuth2Widget authOAuth2Widget
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () => WidgetFor m ()
@ -74,7 +71,6 @@ authOAuth2Widget = buildPlugin fetchAccessToken
-- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2' -- | A version of 'authOAuth2Widget' that uses 'fetchAccessToken2'
-- --
-- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129> -- See <https://github.com/thoughtbot/yesod-auth-oauth2/pull/129>
--
authOAuth2Widget' authOAuth2Widget'
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () => WidgetFor m ()
@ -92,11 +88,13 @@ buildPlugin
-> OAuth2 -> OAuth2
-> FetchCreds m -> FetchCreds m
-> AuthPlugin m -> AuthPlugin m
buildPlugin getToken widget name oauth getCreds = AuthPlugin buildPlugin getToken widget name oauth getCreds =
name AuthPlugin
(dispatchAuthRequest name oauth getToken getCreds) name
login (dispatchAuthRequest name oauth getToken getCreds)
where login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|] login
where
login tm = [whamlet|<a href=@{tm $ oauth2Url name}>^{widget}|]
-- | Read the @'AccessToken'@ from the values set via @'setExtra'@ -- | Read the @'AccessToken'@ from the values set via @'setExtra'@
getAccessToken :: Creds m -> Maybe AccessToken getAccessToken :: Creds m -> Maybe AccessToken
@ -105,7 +103,6 @@ getAccessToken = (AccessToken <$>) . lookup "accessToken" . credsExtra
-- | Read the @'RefreshToken'@ from the values set via @'setExtra'@ -- | Read the @'RefreshToken'@ from the values set via @'setExtra'@
-- --
-- N.B. not all providers supply this value. -- N.B. not all providers supply this value.
--
getRefreshToken :: Creds m -> Maybe RefreshToken getRefreshToken :: Creds m -> Maybe RefreshToken
getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra getRefreshToken = (RefreshToken <$>) . lookup "refreshToken" . credsExtra

View File

@ -1,10 +1,10 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- OAuth2 plugin for <https://auth0.com> -- OAuth2 plugin for <https://auth0.com>
-- --
-- * Authenticates against specific auth0 tenant -- * 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 -- * 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 module Yesod.Auth.OAuth2.Auth0
( oauth2Auth0HostScopes ( oauth2Auth0HostScopes
, oauth2Auth0Host , oauth2Auth0Host
@ -13,8 +13,8 @@ module Yesod.Auth.OAuth2.Auth0
import Data.Aeson as Aeson import Data.Aeson as Aeson
import qualified Data.Text as T import qualified Data.Text as T
import Prelude
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Prelude
-- | https://auth0.com/docs/api/authentication#get-user-info -- | https://auth0.com/docs/api/authentication#get-user-info
newtype User = User T.Text newtype User = User T.Text
@ -36,22 +36,25 @@ oauth2Auth0HostScopes
:: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m :: YesodAuth m => URI -> [Text] -> Text -> Text -> AuthPlugin m
oauth2Auth0HostScopes host scopes clientId clientSecret = oauth2Auth0HostScopes host scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User uid, userResponse) <- authGetProfile (User uid, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
(host `withPath` "/userinfo") token
pure Creds (host `withPath` "/userinfo")
{ credsPlugin = pluginName pure
, credsIdent = uid Creds
, credsExtra = setExtra token userResponse { credsPlugin = pluginName
} , credsIdent = uid
, credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
host `withPath` "/authorize" `withQuery` [scopeParam " " scopes] , oauth2AuthorizeEndpoint =
, oauth2TokenEndpoint = host `withPath` "/oauth/token" host `withPath` "/authorize" `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = host `withPath` "/oauth/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,18 +1,18 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for Azure AD. -- OAuth2 plugin for Azure AD.
-- --
-- * Authenticates against Azure AD -- * Authenticates against Azure AD
-- * Uses email as credentials identifier -- * Uses email as credentials identifier
--
module Yesod.Auth.OAuth2.AzureAD module Yesod.Auth.OAuth2.AzureAD
( oauth2AzureAD ( oauth2AzureAD
, oauth2AzureADScoped , oauth2AzureADScoped
) where ) where
import Prelude
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Prelude
newtype User = User Text newtype User = User Text
@ -31,26 +31,29 @@ oauth2AzureAD = oauth2AzureADScoped defaultScopes
oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2AzureADScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2AzureADScoped scopes clientId clientSecret = oauth2AzureADScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://graph.microsoft.com/v1.0/me" token
"https://graph.microsoft.com/v1.0/me"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://login.windows.net/common/oauth2/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [ scopeParam "," scopes "https://login.windows.net/common/oauth2/authorize"
, ("resource", "https://graph.microsoft.com") `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
}

View File

@ -1,18 +1,18 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for Azure AD using the new v2 endpoints. -- OAuth2 plugin for Azure AD using the new v2 endpoints.
-- --
-- * Authenticates against Azure AD -- * Authenticates against Azure AD
-- * Uses email as credentials identifier -- * Uses email as credentials identifier
--
module Yesod.Auth.OAuth2.AzureADv2 module Yesod.Auth.OAuth2.AzureADv2
( oauth2AzureADv2 ( oauth2AzureADv2
, oauth2AzureADv2Scoped , oauth2AzureADv2Scoped
) where ) where
import Prelude
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Prelude
import Data.String import Data.String
import Data.Text (unpack) import Data.Text (unpack)
@ -34,49 +34,55 @@ oauth2AzureADv2
-- ^ Tenant Id -- ^ Tenant Id
-- --
-- If using a multi-tenant App, @common@ can be given here. -- If using a multi-tenant App, @common@ can be given here.
-- -> Text
-> Text -- ^ Client Id -- ^ Client Id
-> Text -- ^ Client secret -> Text
-- ^ Client secret
-> AuthPlugin m -> AuthPlugin m
oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes oauth2AzureADv2 = oauth2AzureADv2Scoped defaultScopes
oauth2AzureADv2Scoped oauth2AzureADv2Scoped
:: YesodAuth m :: YesodAuth m
=> [Text] -- ^ Scopes => [Text]
-- ^ Scopes
-> Text -> Text
-- ^ Tenant Id -- ^ Tenant Id
-- --
-- If using a multi-tenant App, @common@ can be given here. -- If using a multi-tenant App, @common@ can be given here.
-- -> Text
-> Text -- ^ Client Id -- ^ Client Id
-> Text -- ^ Client Secret -> Text
-- ^ Client Secret
-> AuthPlugin m -> AuthPlugin m
oauth2AzureADv2Scoped scopes tenantId clientId clientSecret = oauth2AzureADv2Scoped scopes tenantId clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://graph.microsoft.com/v1.0/me" token
"https://graph.microsoft.com/v1.0/me"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
tenantUrl "/authorize" `withQuery` [scopeParam " " scopes] , oauth2AuthorizeEndpoint =
, oauth2TokenEndpoint = tenantUrl "/token" tenantUrl "/authorize" `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = tenantUrl "/token"
} , oauth2RedirectUri = Nothing
}
tenantUrl path = tenantUrl path =
fromString fromString $
$ "https://login.microsoftonline.com/" "https://login.microsoftonline.com/"
<> unpack tenantId <> unpack tenantId
<> "/oauth2/v2.0" <> "/oauth2/v2.0"
<> path <> path

View File

@ -7,7 +7,6 @@
-- * Authenticates against battle.net. -- * Authenticates against battle.net.
-- * Uses user's id as credentials identifier. -- * Uses user's id as credentials identifier.
-- * Returns user's battletag in extras. -- * Returns user's battletag in extras.
--
module Yesod.Auth.OAuth2.BattleNet module Yesod.Auth.OAuth2.BattleNet
( oauth2BattleNet ( oauth2BattleNet
, oAuth2BattleNet , oAuth2BattleNet
@ -28,32 +27,37 @@ pluginName = "battle.net"
oauth2BattleNet oauth2BattleNet
:: YesodAuth m :: YesodAuth m
=> WidgetFor m () -- ^ Login widget => WidgetFor m ()
-> Text -- ^ User region (e.g. "eu", "cn", "us") -- ^ Login widget
-> Text -- ^ Client ID -> Text
-> Text -- ^ Client Secret -- ^ User region (e.g. "eu", "cn", "us")
-> Text
-- ^ Client ID
-> Text
-- ^ Client Secret
-> AuthPlugin m -> AuthPlugin m
oauth2BattleNet widget region clientId clientSecret = oauth2BattleNet widget region clientId clientSecret =
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- (User userId, userResponse) <-
authGetProfile pluginName manager token authGetProfile pluginName manager token $
$ fromRelative "https" (apiHost $ T.toLower region) "/account/user" fromRelative "https" (apiHost $ T.toLower region) "/account/user"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = T.pack $ show userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
host = wwwHost $ T.toLower region host = wwwHost $ T.toLower region
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize" , oauth2ClientSecret = Just clientSecret
, oauth2TokenEndpoint = fromRelative "https" host "/oauth/token" , oauth2AuthorizeEndpoint = fromRelative "https" host "/oauth/authorize"
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = fromRelative "https" host "/oauth/token"
} , oauth2RedirectUri = Nothing
}
apiHost :: Text -> Host apiHost :: Text -> Host
apiHost "cn" = "api.battlenet.com.cn" apiHost "cn" = "api.battlenet.com.cn"

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://bitbucket.com -- OAuth2 plugin for http://bitbucket.com
-- --
-- * Authenticates against bitbucket -- * Authenticates against bitbucket
-- * Uses bitbucket uuid as credentials identifier -- * Uses bitbucket uuid as credentials identifier
--
module Yesod.Auth.OAuth2.Bitbucket module Yesod.Auth.OAuth2.Bitbucket
( oauth2Bitbucket ( oauth2Bitbucket
, oauth2BitbucketScoped , oauth2BitbucketScoped
@ -32,30 +32,33 @@ oauth2Bitbucket = oauth2BitbucketScoped defaultScopes
oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2BitbucketScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2BitbucketScoped scopes clientId clientSecret = oauth2BitbucketScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://api.bitbucket.com/2.0/user" token
"https://api.bitbucket.com/2.0/user"
pure Creds pure
{ credsPlugin = pluginName Creds
-- FIXME: Preserved bug. This should just be userId (it's already { credsPlugin = pluginName
-- a Text), but because this code was shipped, folks likely have , -- FIXME: Preserved bug. This should just be userId (it's already
-- Idents in their database like @"\"...\""@, and if we fixed this -- a Text), but because this code was shipped, folks likely have
-- they would need migrating. We're keeping it for now as it's a -- Idents in their database like @"\"...\""@, and if we fixed this
-- minor wart. Breaking typed APIs is one thing, causing data to go -- they would need migrating. We're keeping it for now as it's a
-- invalid is another. -- minor wart. Breaking typed APIs is one thing, causing data to go
, credsIdent = T.pack $ show userId -- invalid is another.
, credsExtra = setExtra token userResponse credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://bitbucket.com/site/oauth2/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam "," scopes] "https://bitbucket.com/site/oauth2/authorize"
, oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token" `withQuery` [scopeParam "," scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://bitbucket.com/site/oauth2/access_token"
} , oauth2RedirectUri = Nothing
}

View File

@ -26,24 +26,27 @@ oauth2ClassLink = oauth2ClassLinkScoped defaultScopes
oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2ClassLinkScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2ClassLinkScoped scopes clientId clientSecret = oauth2ClassLinkScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://nodeapi.classlink.com/v2/my/info" token
"https://nodeapi.classlink.com/v2/my/info"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = T.pack $ show userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://launchpad.classlink.com/oauth2/v2/auth" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam "," scopes] "https://launchpad.classlink.com/oauth2/v2/auth"
, oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token" `withQuery` [scopeParam "," scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://launchpad.classlink.com/oauth2/v2/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -18,8 +18,8 @@ import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Network.HTTP.Conduit (Manager) import Network.HTTP.Conduit (Manager)
import Network.OAuth.OAuth2.Compat import Network.OAuth.OAuth2.Compat
import UnliftIO.Exception
import URI.ByteString.Extension import URI.ByteString.Extension
import UnliftIO.Exception
import Yesod.Auth hiding (ServerError) import Yesod.Auth hiding (ServerError)
import Yesod.Auth.OAuth2.DispatchError import Yesod.Auth.OAuth2.DispatchError
import Yesod.Auth.OAuth2.ErrorResponse import Yesod.Auth.OAuth2.ErrorResponse
@ -29,21 +29,26 @@ import Yesod.Core hiding (ErrorResponse)
-- | How to fetch an @'OAuth2Token'@ -- | How to fetch an @'OAuth2Token'@
-- --
-- This will be 'fetchAccessToken' or 'fetchAccessToken2' -- This will be 'fetchAccessToken' or 'fetchAccessToken2'
-- type FetchToken =
type FetchToken Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)
= Manager -> OAuth2 -> ExchangeToken -> IO (OAuth2Result Errors OAuth2Token)
-- | How to take an @'OAuth2Token'@ and retrieve user credentials -- | How to take an @'OAuth2Token'@ and retrieve user credentials
type FetchCreds m = Manager -> OAuth2Token -> IO (Creds m) type FetchCreds m = Manager -> OAuth2Token -> IO (Creds m)
-- | Dispatch the various OAuth2 handshake routes -- | Dispatch the various OAuth2 handshake routes
dispatchAuthRequest dispatchAuthRequest
:: Text -- ^ Name :: Text
-> OAuth2 -- ^ Service details -- ^ Name
-> FetchToken -- ^ How to get a token -> OAuth2
-> FetchCreds m -- ^ How to get credentials -- ^ Service details
-> Text -- ^ Method -> FetchToken
-> [Text] -- ^ Path pieces -- ^ How to get a token
-> FetchCreds m
-- ^ How to get credentials
-> Text
-- ^ Method
-> [Text]
-- ^ Path pieces
-> AuthHandler m TypedContent -> AuthHandler m TypedContent
dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] = dispatchAuthRequest name oauth2 _ _ "GET" ["forward"] =
handleDispatchError $ dispatchForward name oauth2 handleDispatchError $ dispatchForward name oauth2
@ -55,7 +60,6 @@ dispatchAuthRequest _ _ _ _ _ _ = notFound
-- --
-- 1. Set a random CSRF token in our session -- 1. Set a random CSRF token in our session
-- 2. Redirect to the Provider's authorization URL -- 2. Redirect to the Provider's authorization URL
--
dispatchForward dispatchForward
:: (MonadError DispatchError m, MonadAuthHandler site m) :: (MonadError DispatchError m, MonadAuthHandler site m)
=> Text => Text
@ -71,7 +75,6 @@ dispatchForward name oauth2 = do
-- 1. Verify the URL's CSRF token matches our session -- 1. Verify the URL's CSRF token matches our session
-- 2. Use the code parameter to fetch an AccessToken for the Provider -- 2. Use the code parameter to fetch an AccessToken for the Provider
-- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider -- 3. Use the AccessToken to construct a @'Creds'@ value for the Provider
--
dispatchCallback dispatchCallback
:: (MonadError DispatchError m, MonadAuthHandler site m) :: (MonadError DispatchError m, MonadAuthHandler site m)
=> Text => Text
@ -85,12 +88,13 @@ dispatchCallback name oauth2 getToken getCreds = do
code <- requireGetParam "code" code <- requireGetParam "code"
manager <- authHttpManager manager <- authHttpManager
oauth2' <- withCallbackAndState name oauth2 csrf oauth2' <- withCallbackAndState name oauth2 csrf
token <- either (throwError . OAuth2ResultError) pure token <-
=<< liftIO (getToken manager oauth2' $ ExchangeToken code) either (throwError . OAuth2ResultError) pure
=<< liftIO (getToken manager oauth2' $ ExchangeToken code)
creds <- creds <-
liftIO (getCreds manager token) liftIO (getCreds manager token)
`catch` (throwError . FetchCredsIOException) `catch` (throwError . FetchCredsIOException)
`catch` (throwError . FetchCredsYesodOAuth2Exception) `catch` (throwError . FetchCredsYesodOAuth2Exception)
setCredsRedirect creds setCredsRedirect creds
withCallbackAndState withCallbackAndState
@ -102,11 +106,12 @@ withCallbackAndState
withCallbackAndState name oauth2 csrf = do withCallbackAndState name oauth2 csrf = do
uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender uri <- ($ PluginR name ["callback"]) <$> getParentUrlRender
callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri callback <- maybe (throwError $ InvalidCallbackUri uri) pure $ fromText uri
pure oauth2 pure
{ oauth2RedirectUri = Just callback oauth2
, oauth2AuthorizeEndpoint = { oauth2RedirectUri = Just callback
oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)] , oauth2AuthorizeEndpoint =
} oauth2AuthorizeEndpoint oauth2 `withQuery` [("state", encodeUtf8 csrf)]
}
getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text) getParentUrlRender :: MonadHandler m => m (Route (SubHandlerSite m) -> Text)
getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent
@ -119,12 +124,12 @@ getParentUrlRender = (.) <$> getUrlRender <*> getRouteToParent
-- --
-- Therefore, we just exclude @+@ in our tokens, which means this function may -- Therefore, we just exclude @+@ in our tokens, which means this function may
-- return slightly less than 30 characters. -- return slightly less than 30 characters.
--
setSessionCSRF :: MonadHandler m => Text -> m Text setSessionCSRF :: MonadHandler m => Text -> m Text
setSessionCSRF sessionKey = do setSessionCSRF sessionKey = do
csrfToken <- liftIO randomToken csrfToken <- liftIO randomToken
csrfToken <$ setSession sessionKey csrfToken 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 -- | Verify the callback provided the same CSRF token as in our session
verifySessionCSRF verifySessionCSRF
@ -133,9 +138,10 @@ verifySessionCSRF sessionKey = do
token <- requireGetParam "state" token <- requireGetParam "state"
sessionToken <- lookupSession sessionKey sessionToken <- lookupSession sessionKey
deleteSession sessionKey deleteSession sessionKey
token <$ unless token
(sessionToken == Just token) <$ unless
(throwError $ InvalidStateToken sessionToken token) (sessionToken == Just token)
(throwError $ InvalidStateToken sessionToken token)
requireGetParam requireGetParam
:: (MonadError DispatchError m, MonadHandler m) => Text -> m Text :: (MonadError DispatchError m, MonadHandler m) => Text -> m Text

View File

@ -9,7 +9,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
module Yesod.Auth.OAuth2.DispatchError module Yesod.Auth.OAuth2.DispatchError
( DispatchError(..) ( DispatchError (..)
, handleDispatchError , handleDispatchError
, onDispatchError , onDispatchError
) where ) where
@ -26,34 +26,33 @@ import Yesod.Auth.OAuth2.Random
import Yesod.Core hiding (ErrorResponse) import Yesod.Core hiding (ErrorResponse)
data DispatchError data DispatchError
= MissingParameter Text = MissingParameter Text
| InvalidStateToken (Maybe Text) Text | InvalidStateToken (Maybe Text) Text
| InvalidCallbackUri Text | InvalidCallbackUri Text
| OAuth2HandshakeError ErrorResponse | OAuth2HandshakeError ErrorResponse
| OAuth2ResultError Errors | OAuth2ResultError Errors
| FetchCredsIOException IOException | FetchCredsIOException IOException
| FetchCredsYesodOAuth2Exception YesodOAuth2Exception | FetchCredsYesodOAuth2Exception YesodOAuth2Exception
| OtherDispatchError Text | OtherDispatchError Text
deriving stock Show deriving stock (Show)
deriving anyclass Exception deriving anyclass (Exception)
-- | User-friendly message for any given 'DispatchError' -- | User-friendly message for any given 'DispatchError'
-- --
-- Most of these are opaque to the user. The exception details are present for -- Most of these are opaque to the user. The exception details are present for
-- the server logs. -- the server logs.
--
dispatchErrorMessage :: DispatchError -> Text dispatchErrorMessage :: DispatchError -> Text
dispatchErrorMessage = \case dispatchErrorMessage = \case
MissingParameter name -> MissingParameter name ->
"Parameter '" <> name <> "' is required, but not present in the URL" "Parameter '" <> name <> "' is required, but not present in the URL"
InvalidStateToken{} -> "State token is invalid, please try again" InvalidStateToken {} -> "State token is invalid, please try again"
InvalidCallbackUri{} -> InvalidCallbackUri {} ->
"Callback URI was not valid, this server may be misconfigured (no approot)" "Callback URI was not valid, this server may be misconfigured (no approot)"
OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er OAuth2HandshakeError er -> "OAuth2 handshake failure: " <> erUserMessage er
OAuth2ResultError{} -> "Login failed, please try again" OAuth2ResultError {} -> "Login failed, please try again"
FetchCredsIOException{} -> "Login failed, please try again" FetchCredsIOException {} -> "Login failed, please try again"
FetchCredsYesodOAuth2Exception{} -> "Login failed, please try again" FetchCredsYesodOAuth2Exception {} -> "Login failed, please try again"
OtherDispatchError{} -> "Login failed, please try again" OtherDispatchError {} -> "Login failed, please try again"
handleDispatchError handleDispatchError
:: MonadAuthHandler site m :: MonadAuthHandler site m

View File

@ -1,12 +1,12 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | OAuth callback error response -- | OAuth callback error response
-- --
-- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1> -- <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>
--
module Yesod.Auth.OAuth2.ErrorResponse module Yesod.Auth.OAuth2.ErrorResponse
( ErrorResponse(..) ( ErrorResponse (..)
, erUserMessage , erUserMessage
, ErrorName(..) , ErrorName (..)
, onErrorResponse , onErrorResponse
, unknownError , unknownError
) where ) where
@ -17,22 +17,22 @@ import Data.Traversable (for)
import Yesod.Core (MonadHandler, lookupGetParam) import Yesod.Core (MonadHandler, lookupGetParam)
data ErrorName data ErrorName
= InvalidRequest = InvalidRequest
| UnauthorizedClient | UnauthorizedClient
| AccessDenied | AccessDenied
| UnsupportedResponseType | UnsupportedResponseType
| InvalidScope | InvalidScope
| ServerError | ServerError
| TemporarilyUnavailable | TemporarilyUnavailable
| Unknown Text | Unknown Text
deriving Show deriving (Show)
data ErrorResponse = ErrorResponse data ErrorResponse = ErrorResponse
{ erName :: ErrorName { erName :: ErrorName
, erDescription :: Maybe Text , erDescription :: Maybe Text
, erURI :: Maybe Text , erURI :: Maybe Text
} }
deriving Show deriving (Show)
-- | Textual value suitable for display to a User -- | Textual value suitable for display to a User
erUserMessage :: ErrorResponse -> Text erUserMessage :: ErrorResponse -> Text
@ -48,13 +48,12 @@ erUserMessage err = case erName err of
unknownError :: Text -> ErrorResponse unknownError :: Text -> ErrorResponse
unknownError x = 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 -- | Check query parameters for an error, if found run the given action
-- --
-- The action is expected to use a short-circuit response function like -- The action is expected to use a short-circuit response function like
-- @'permissionDenied'@, hence this returning @()@. -- @'permissionDenied'@, hence this returning @()@.
--
onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m () onErrorResponse :: MonadHandler m => (ErrorResponse -> m a) -> m ()
onErrorResponse f = traverse_ f =<< checkErrorResponse onErrorResponse f = traverse_ f =<< checkErrorResponse

View File

@ -1,16 +1,16 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- OAuth2 plugin for http://eveonline.com -- OAuth2 plugin for http://eveonline.com
-- --
-- * Authenticates against eveonline -- * Authenticates against eveonline
-- * Uses EVEs unique account-user-char-hash as credentials identifier -- * Uses EVEs unique account-user-char-hash as credentials identifier
--
module Yesod.Auth.OAuth2.EveOnline module Yesod.Auth.OAuth2.EveOnline
( oauth2Eve ( oauth2Eve
, oauth2EveScoped , oauth2EveScoped
, WidgetType(..) , WidgetType (..)
) where ) where
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
@ -24,23 +24,24 @@ instance FromJSON User where
parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash" parseJSON = withObject "User" $ \o -> User <$> o .: "CharacterOwnerHash"
data WidgetType m data WidgetType m
= Plain -- ^ Simple "Login via eveonline" text = -- | Simple "Login via eveonline" text
| BigWhite Plain
| SmallWhite | BigWhite
| BigBlack | SmallWhite
| SmallBlack | BigBlack
| Custom (WidgetFor m ()) | SmallBlack
| Custom (WidgetFor m ())
asWidget :: YesodAuth m => WidgetType m -> WidgetFor m () asWidget :: YesodAuth m => WidgetType m -> WidgetFor m ()
asWidget Plain = [whamlet|Login via eveonline|] asWidget Plain = [whamlet|Login via eveonline|]
asWidget BigWhite = asWidget BigWhite =
[whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|] [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4PTzeiAshqiM8osU2giO0Y/5cc4cb60bac52422da2e45db87b6819c/EVE_SSO_Login_Buttons_Large_White.png?w=270&h=45">|]
asWidget BigBlack asWidget BigBlack =
= [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|] [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/4fSjj56uD6CYwYyus4KmES/4f6385c91e6de56274d99496e6adebab/EVE_SSO_Login_Buttons_Large_Black.png?w=270&h=45">|]
asWidget SmallWhite asWidget SmallWhite =
= [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|] [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/18BxKSXCymyqY4QKo8KwKe/c2bdded6118472dd587c8107f24104d7/EVE_SSO_Login_Buttons_Small_White.png?w=195&h=30">|]
asWidget SmallBlack asWidget SmallBlack =
= [whamlet|<img src="https://images.contentful.com/idjq7aai9ylm/12vrPsIMBQi28QwCGOAqGk/33234da7672c6b0cdca394fc8e0b1c2b/EVE_SSO_Login_Buttons_Small_Black.png?w=195&h=30">|] [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 asWidget (Custom a) = a
pluginName :: Text pluginName :: Text
@ -57,25 +58,28 @@ oauth2EveScoped
oauth2EveScoped scopes widgetType clientId clientSecret = oauth2EveScoped scopes widgetType clientId clientSecret =
authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token -> authOAuth2Widget (asWidget widgetType) pluginName oauth2 $ \manager token ->
do do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://login.eveonline.com/oauth/verify" token
"https://login.eveonline.com/oauth/verify"
pure Creds pure
{ credsPlugin = "eveonline" Creds
-- FIXME: Preserved bug. See similar comment in Bitbucket provider. { credsPlugin = "eveonline"
, credsIdent = T.pack $ show userId , -- FIXME: Preserved bug. See similar comment in Bitbucket provider.
, credsExtra = setExtra token userResponse credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://login.eveonline.com/oauth/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [("response_type", "code"), scopeParam " " scopes] "https://login.eveonline.com/oauth/authorize"
, oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token" `withQuery` [("response_type", "code"), scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://login.eveonline.com/oauth/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,7 +1,7 @@
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveDataTypeable #-}
module Yesod.Auth.OAuth2.Exception module Yesod.Auth.OAuth2.Exception
( YesodOAuth2Exception(..) ( YesodOAuth2Exception (..)
) where ) where
import Control.Exception.Safe import Control.Exception.Safe
@ -9,21 +9,18 @@ import Data.ByteString.Lazy (ByteString)
import Data.Text (Text) import Data.Text (Text)
data YesodOAuth2Exception data YesodOAuth2Exception
= OAuth2Error Text ByteString = -- | HTTP error during OAuth2 handshake
-- ^ HTTP error during OAuth2 handshake
-- --
-- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@. -- Plugin name and JSON-encoded @OAuth2Error@ from @hoauth2@.
-- OAuth2Error Text ByteString
| JSONDecodingError Text String | -- | User profile was not as expected
-- ^ User profile was not as expected
-- --
-- Plugin name and Aeson parse error message. -- Plugin name and Aeson parse error message.
-- JSONDecodingError Text String
| GenericError Text String | -- | Other error conditions
-- ^ Other error conditions
-- --
-- Plugin name and error message. -- Plugin name and error message.
-- GenericError Text String
deriving (Show, Typeable) deriving (Show, Typeable)
instance Exception YesodOAuth2Exception instance Exception YesodOAuth2Exception

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://github.com -- OAuth2 plugin for http://github.com
-- --
-- * Authenticates against github -- * Authenticates against github
-- * Uses github user id as credentials identifier -- * Uses github user id as credentials identifier
--
module Yesod.Auth.OAuth2.GitHub module Yesod.Auth.OAuth2.GitHub
( oauth2GitHub ( oauth2GitHub
, oauth2GitHubScoped , oauth2GitHubScoped
@ -32,24 +32,27 @@ oauth2GitHub = oauth2GitHubScoped defaultScopes
oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2GitHubScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2GitHubScoped scopes clientId clientSecret = oauth2GitHubScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://api.github.com/user" token
"https://api.github.com/user"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = T.pack $ show userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://github.com/login/oauth/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam "," scopes] "https://github.com/login/oauth/authorize"
, oauth2TokenEndpoint = "https://github.com/login/oauth/access_token" `withQuery` [scopeParam "," scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://github.com/login/oauth/access_token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OAuth2.GitLab module Yesod.Auth.OAuth2.GitLab
( oauth2GitLab ( oauth2GitLab
, oauth2GitLabHostScopes , oauth2GitLabHostScopes
@ -32,7 +33,6 @@ defaultScopes = ["read_user"]
-- --
-- > oauth2GitLabHostScopes defaultHost ["api", "read_user"] -- > oauth2GitLabHostScopes defaultHost ["api", "read_user"]
-- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes -- > oauth2GitLabHostScopes "https://gitlab.example.com" defaultScopes
--
oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2GitLab :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes oauth2GitLab = oauth2GitLabHostScopes defaultHost defaultScopes
@ -43,17 +43,19 @@ oauth2GitLabHostScopes host scopes clientId clientSecret =
(User userId, userResponse) <- (User userId, userResponse) <-
authGetProfile pluginName manager token $ host `withPath` "/api/v4/user" authGetProfile pluginName manager token $ host `withPath` "/api/v4/user"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = T.pack $ show userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes] , oauth2AuthorizeEndpoint =
, oauth2TokenEndpoint = host `withPath` "/oauth/token" host `withPath` "/oauth/authorize" `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = host `withPath` "/oauth/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,5 +1,6 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
-- | -- |
-- --
-- OAuth2 plugin for http://www.google.com -- OAuth2 plugin for http://www.google.com
@ -23,7 +24,6 @@
-- > updatedCreds = creds { credsIdent = email } -- > updatedCreds = creds { credsIdent = email }
-- > -- >
-- > -- continue normally with updatedCreds -- > -- continue normally with updatedCreds
--
module Yesod.Auth.OAuth2.Google module Yesod.Auth.OAuth2.Google
( oauth2Google ( oauth2Google
, oauth2GoogleWidget , oauth2GoogleWidget
@ -38,9 +38,10 @@ newtype User = User Text
instance FromJSON User where instance FromJSON User where
parseJSON = parseJSON =
withObject "User" $ \o -> User withObject "User" $ \o ->
-- Required for data backwards-compatibility User
<$> (("google-uid:" <>) <$> o .: "sub") -- Required for data backwards-compatibility
<$> (("google-uid:" <>) <$> o .: "sub")
pluginName :: Text pluginName :: Text
pluginName = "google" pluginName = "google"
@ -63,24 +64,27 @@ oauth2GoogleScopedWidget
:: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m :: YesodAuth m => WidgetFor m () -> [Text] -> Text -> Text -> AuthPlugin m
oauth2GoogleScopedWidget widget scopes clientId clientSecret = oauth2GoogleScopedWidget widget scopes clientId clientSecret =
authOAuth2Widget widget pluginName oauth2 $ \manager token -> do authOAuth2Widget widget pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://www.googleapis.com/oauth2/v3/userinfo" token
"https://www.googleapis.com/oauth2/v3/userinfo"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://accounts.google.com/o/oauth2/auth" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam " " scopes] "https://accounts.google.com/o/oauth2/auth"
, oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token" `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://www.googleapis.com/oauth2/v3/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -26,41 +26,45 @@ defaultScopes = ["email"]
oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Nylas :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Nylas clientId clientSecret = oauth2Nylas clientId clientSecret =
authOAuth2 pluginName oauth $ \manager token -> do authOAuth2 pluginName oauth $ \manager token -> do
req <- applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) "" req <-
<$> parseRequest "https://api.nylas.com/account" applyBasicAuth (encodeUtf8 $ atoken $ accessToken token) ""
<$> parseRequest "https://api.nylas.com/account"
resp <- httpLbs req manager resp <- httpLbs req manager
let userResponse = responseBody resp let userResponse = responseBody resp
-- FIXME: was this working? I'm 95% sure that the client will throw its -- FIXME: was this working? I'm 95% sure that the client will throw its
-- own exception on unsuccessful status codes. -- own exception on unsuccessful status codes.
unless (HT.statusIsSuccessful $ responseStatus resp) unless (HT.statusIsSuccessful $ responseStatus resp) $
$ throwIO throwIO $
$ YesodOAuth2Exception.GenericError pluginName YesodOAuth2Exception.GenericError pluginName $
$ "Unsuccessful HTTP response: " "Unsuccessful HTTP response: "
<> BL8.unpack userResponse <> BL8.unpack userResponse
either either
(throwIO . YesodOAuth2Exception.JSONDecodingError pluginName) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
(\(User userId) -> pure Creds ( \(User userId) ->
{ credsPlugin = pluginName pure
, credsIdent = userId Creds
, credsExtra = setExtra token userResponse { credsPlugin = pluginName
} , credsIdent = userId
) , credsExtra = setExtra token userResponse
}
)
$ eitherDecode userResponse $ eitherDecode userResponse
where where
oauth = OAuth2 oauth =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://api.nylas.com/oauth/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [ ("response_type", "code") "https://api.nylas.com/oauth/authorize"
, ("client_id", encodeUtf8 clientId) `withQuery` [ ("response_type", "code")
-- N.B. The scopes delimeter is unknown/untested. Verify that before , ("client_id", encodeUtf8 clientId)
-- extracting this to an argument and offering a Scoped function. In , -- N.B. The scopes delimeter is unknown/untested. Verify that before
-- its current state, it doesn't matter because it's only one scope. -- extracting this to an argument and offering a Scoped function. In
, scopeParam "," defaultScopes -- its current state, it doesn't matter because it's only one scope.
] scopeParam "," defaultScopes
, oauth2TokenEndpoint = "https://api.nylas.com/oauth/token" ]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://api.nylas.com/oauth/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,10 +1,10 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
-- | -- |
-- --
-- Modules and support functions required by most or all provider -- Modules and support functions required by most or all provider
-- implementations. May also be useful for writing local providers. -- implementations. May also be useful for writing local providers.
--
module Yesod.Auth.OAuth2.Prelude module Yesod.Auth.OAuth2.Prelude
( authGetProfile ( authGetProfile
, scopeParam , scopeParam
@ -20,8 +20,8 @@ module Yesod.Auth.OAuth2.Prelude
, (.:?) , (.:?)
, (.=) , (.=)
, (<>) , (<>)
, FromJSON(..) , FromJSON (..)
, ToJSON(..) , ToJSON (..)
, eitherDecode , eitherDecode
, withObject , withObject
@ -29,22 +29,22 @@ module Yesod.Auth.OAuth2.Prelude
, throwIO , throwIO
-- * OAuth2 -- * OAuth2
, OAuth2(..) , OAuth2 (..)
, OAuth2Token(..) , OAuth2Token (..)
, AccessToken(..) , AccessToken (..)
, RefreshToken(..) , RefreshToken (..)
-- * HTTP -- * HTTP
, Manager , Manager
-- * Yesod -- * Yesod
, YesodAuth(..) , YesodAuth (..)
, AuthPlugin(..) , AuthPlugin (..)
, Creds(..) , Creds (..)
-- * Bytestring URI types -- * Bytestring URI types
, URI , URI
, Host(..) , Host (..)
-- * Bytestring URI extensions -- * Bytestring URI extensions
, module URI.ByteString.Extension , module URI.ByteString.Extension
@ -74,7 +74,6 @@ import qualified Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
-- The response should be parsed only far enough to read the required -- The response should be parsed only far enough to read the required
-- @'credsIdent'@. Additional information should either be re-parsed by or -- @'credsIdent'@. Additional information should either be re-parsed by or
-- fetched via additional requests by consumers. -- fetched via additional requests by consumers.
--
authGetProfile authGetProfile
:: FromJSON a :: FromJSON a
=> Text => Text
@ -101,7 +100,7 @@ fromAuthJSON name =
-- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter -- | A tuple of @\"scope\"@ and the given scopes separated by a delimiter
scopeParam :: Text -> [Text] -> (ByteString, ByteString) scopeParam :: Text -> [Text] -> (ByteString, ByteString)
scopeParam d = ("scope", ) . encodeUtf8 . T.intercalate d scopeParam d = ("scope",) . encodeUtf8 . T.intercalate d
-- brittany-disable-next-binding -- brittany-disable-next-binding
@ -115,10 +114,9 @@ scopeParam d = ("scope", ) . encodeUtf8 . T.intercalate d
-- May set the following keys: -- May set the following keys:
-- --
-- - @refreshToken@: if the provider supports refreshing the @accessToken@ -- - @refreshToken@: if the provider supports refreshing the @accessToken@
--
setExtra :: OAuth2Token -> BL.ByteString -> [(Text, Text)] setExtra :: OAuth2Token -> BL.ByteString -> [(Text, Text)]
setExtra token userResponse = setExtra token userResponse =
[ ("accessToken", atoken $ accessToken token) [ ("accessToken", atoken $ accessToken token)
, ("userResponse", decodeUtf8 $ BL.toStrict userResponse) , ("userResponse", decodeUtf8 $ BL.toStrict userResponse)
] ]
<> maybe [] (pure . ("refreshToken", ) . rtoken) (refreshToken token) <> maybe [] (pure . ("refreshToken",) . rtoken) (refreshToken token)

View File

@ -5,7 +5,7 @@ module Yesod.Auth.OAuth2.Random
) where ) where
import Crypto.Random (MonadRandom, getRandomBytes) 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.ByteString (ByteString)
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8) import Data.Text.Encoding (decodeUtf8)
@ -13,7 +13,7 @@ import Data.Text.Encoding (decodeUtf8)
randomText randomText
:: MonadRandom m :: MonadRandom m
=> Int => Int
-- ^ Size in Bytes (note necessarily characters) -- ^ Size in Bytes (note necessarily characters)
-> m Text -> m Text
randomText size = randomText size =
decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size decodeUtf8 . convertToBase @ByteString Base64 <$> getRandomBytes size

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://login.salesforce.com -- OAuth2 plugin for http://login.salesforce.com
-- --
-- * Authenticates against Salesforce (or sandbox) -- * Authenticates against Salesforce (or sandbox)
-- * Uses Salesforce user id as credentials identifier -- * Uses Salesforce user id as credentials identifier
--
module Yesod.Auth.OAuth2.Salesforce module Yesod.Auth.OAuth2.Salesforce
( oauth2Salesforce ( oauth2Salesforce
, oauth2SalesforceScoped , oauth2SalesforceScoped
@ -30,47 +30,54 @@ oauth2Salesforce :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Salesforce = oauth2SalesforceScoped defaultScopes oauth2Salesforce = oauth2SalesforceScoped defaultScopes
oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2SalesforceScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2SalesforceScoped = salesforceHelper oauth2SalesforceScoped =
pluginName salesforceHelper
"https://login.salesforce.com/services/oauth2/userinfo" pluginName
"https://login.salesforce.com/services/oauth2/authorize" "https://login.salesforce.com/services/oauth2/userinfo"
"https://login.salesforce.com/services/oauth2/token" "https://login.salesforce.com/services/oauth2/authorize"
"https://login.salesforce.com/services/oauth2/token"
oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2SalesforceSandbox :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes oauth2SalesforceSandbox = oauth2SalesforceSandboxScoped defaultScopes
oauth2SalesforceSandboxScoped oauth2SalesforceSandboxScoped
:: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2SalesforceSandboxScoped = salesforceHelper oauth2SalesforceSandboxScoped =
(pluginName <> "-sandbox") salesforceHelper
"https://test.salesforce.com/services/oauth2/userinfo" (pluginName <> "-sandbox")
"https://test.salesforce.com/services/oauth2/authorize" "https://test.salesforce.com/services/oauth2/userinfo"
"https://test.salesforce.com/services/oauth2/token" "https://test.salesforce.com/services/oauth2/authorize"
"https://test.salesforce.com/services/oauth2/token"
salesforceHelper salesforceHelper
:: YesodAuth m :: YesodAuth m
=> Text => Text
-> URI -- ^ User profile -> URI
-> URI -- ^ Authorize -- ^ User profile
-> URI -- ^ Token -> URI
-- ^ Authorize
-> URI
-- ^ Token
-> [Text] -> [Text]
-> Text -> Text
-> Text -> Text
-> AuthPlugin m -> AuthPlugin m
salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret salesforceHelper name profileUri authorizeUri tokenUri scopes clientId clientSecret =
= authOAuth2 name oauth2 $ \manager token -> do authOAuth2 name oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile name manager token profileUri (User userId, userResponse) <- authGetProfile name manager token profileUri
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes] , oauth2ClientSecret = Just clientSecret
, oauth2TokenEndpoint = tokenUri , oauth2AuthorizeEndpoint = authorizeUri `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = tokenUri
} , oauth2RedirectUri = Nothing
}

View File

@ -1,12 +1,12 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- OAuth2 plugin for https://slack.com/ -- OAuth2 plugin for https://slack.com/
-- --
-- * Authenticates against slack -- * Authenticates against slack
-- * Uses slack user id as credentials identifier -- * Uses slack user id as credentials identifier
--
module Yesod.Auth.OAuth2.Slack module Yesod.Auth.OAuth2.Slack
( SlackScope(..) ( SlackScope (..)
, oauth2Slack , oauth2Slack
, oauth2SlackScoped , oauth2SlackScoped
) where ) where
@ -14,14 +14,18 @@ module Yesod.Auth.OAuth2.Slack
import Yesod.Auth.OAuth2.Prelude import Yesod.Auth.OAuth2.Prelude
import Network.HTTP.Client import Network.HTTP.Client
(httpLbs, parseUrlThrow, responseBody, setQueryString) ( httpLbs
, parseUrlThrow
, responseBody
, setQueryString
)
import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception import Yesod.Auth.OAuth2.Exception as YesodOAuth2Exception
data SlackScope data SlackScope
= SlackBasicScope = SlackBasicScope
| SlackEmailScope | SlackEmailScope
| SlackTeamScope | SlackTeamScope
| SlackAvatarScope | SlackAvatarScope
scopeText :: SlackScope -> Text scopeText :: SlackScope -> Text
scopeText SlackBasicScope = "identity.basic" scopeText SlackBasicScope = "identity.basic"
@ -50,26 +54,30 @@ oauth2SlackScoped
oauth2SlackScoped scopes clientId clientSecret = oauth2SlackScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
let param = encodeUtf8 $ atoken $ accessToken token let param = encodeUtf8 $ atoken $ accessToken token
req <- setQueryString [("token", Just param)] req <-
<$> parseUrlThrow "https://slack.com/api/users.identity" setQueryString [("token", Just param)]
<$> parseUrlThrow "https://slack.com/api/users.identity"
userResponse <- responseBody <$> httpLbs req manager userResponse <- responseBody <$> httpLbs req manager
either either
(throwIO . YesodOAuth2Exception.JSONDecodingError pluginName) (throwIO . YesodOAuth2Exception.JSONDecodingError pluginName)
(\(User userId) -> pure Creds ( \(User userId) ->
{ credsPlugin = pluginName pure
, credsIdent = userId Creds
, credsExtra = setExtra token userResponse { credsPlugin = pluginName
} , credsIdent = userId
) , credsExtra = setExtra token userResponse
}
)
$ eitherDecode userResponse $ eitherDecode userResponse
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://slack.com/oauth/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam "," $ map scopeText scopes] "https://slack.com/oauth/authorize"
, oauth2TokenEndpoint = "https://slack.com/api/oauth.access" `withQuery` [scopeParam "," $ map scopeText scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://slack.com/api/oauth.access"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,8 +1,8 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://spotify.com -- OAuth2 plugin for http://spotify.com
--
module Yesod.Auth.OAuth2.Spotify module Yesod.Auth.OAuth2.Spotify
( oauth2Spotify ( oauth2Spotify
) where ) where
@ -20,24 +20,27 @@ pluginName = "spotify"
oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2Spotify :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2Spotify scopes clientId clientSecret = oauth2Spotify scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://api.spotify.com/v1/me" token
"https://api.spotify.com/v1/me"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://accounts.spotify.com/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam " " scopes] "https://accounts.spotify.com/authorize"
, oauth2TokenEndpoint = "https://accounts.spotify.com/api/token" `withQuery` [scopeParam " " scopes]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://accounts.spotify.com/api/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://twitch.tv -- OAuth2 plugin for http://twitch.tv
-- --
-- * Authenticates against twitch -- * Authenticates against twitch
-- * Uses twitch user id as credentials identifier -- * Uses twitch user id as credentials identifier
--
module Yesod.Auth.OAuth2.Twitch module Yesod.Auth.OAuth2.Twitch
( oauth2Twitch ( oauth2Twitch
, oauth2TwitchScoped , oauth2TwitchScoped
@ -32,28 +32,31 @@ oauth2Twitch = oauth2TwitchScoped defaultScopes
oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m oauth2TwitchScoped :: YesodAuth m => [Text] -> Text -> Text -> AuthPlugin m
oauth2TwitchScoped scopes clientId clientSecret = oauth2TwitchScoped scopes clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://id.twitch.tv/oauth2/validate" token
"https://id.twitch.tv/oauth2/validate"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://id.twitch.tv/oauth2/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam " " scopes] "https://id.twitch.tv/oauth2/authorize"
, oauth2TokenEndpoint = `withQuery` [scopeParam " " scopes]
"https://id.twitch.tv/oauth2/token" , oauth2TokenEndpoint =
`withQuery` [ ("client_id", T.encodeUtf8 clientId) "https://id.twitch.tv/oauth2/token"
, ("client_secret", T.encodeUtf8 clientSecret) `withQuery` [ ("client_id", T.encodeUtf8 clientId)
] , ("client_secret", T.encodeUtf8 clientSecret)
, oauth2RedirectUri = Nothing ]
} , oauth2RedirectUri = Nothing
}

View File

@ -1,11 +1,11 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
-- | -- |
-- --
-- OAuth2 plugin for http://upcase.com -- OAuth2 plugin for http://upcase.com
-- --
-- * Authenticates against upcase -- * Authenticates against upcase
-- * Uses upcase user id as credentials identifier -- * Uses upcase user id as credentials identifier
--
module Yesod.Auth.OAuth2.Upcase module Yesod.Auth.OAuth2.Upcase
( oauth2Upcase ( oauth2Upcase
) where ) where
@ -27,22 +27,25 @@ pluginName = "upcase"
oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m oauth2Upcase :: YesodAuth m => Text -> Text -> AuthPlugin m
oauth2Upcase clientId clientSecret = oauth2Upcase clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(User userId, userResponse) <- authGetProfile (User userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"http://upcase.com/api/v1/me.json" token
"http://upcase.com/api/v1/me.json"
pure Creds pure
{ credsPlugin = pluginName Creds
, credsIdent = T.pack $ show userId { credsPlugin = pluginName
, credsExtra = setExtra token userResponse , credsIdent = T.pack $ show userId
} , credsExtra = setExtra token userResponse
}
where where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize" , oauth2ClientSecret = Just clientSecret
, oauth2TokenEndpoint = "http://upcase.com/oauth/token" , oauth2AuthorizeEndpoint = "http://upcase.com/oauth/authorize"
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "http://upcase.com/oauth/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -16,31 +16,35 @@ instance FromJSON WpUser where
parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID" parseJSON = withObject "WpUser" $ \o -> WpUser <$> o .: "ID"
oauth2WordPressDotCom oauth2WordPressDotCom
:: (YesodAuth m) :: YesodAuth m
=> Text -- ^ Client Id => Text
-> Text -- ^ Client Secret -- ^ Client Id
-> Text
-- ^ Client Secret
-> AuthPlugin m -> AuthPlugin m
oauth2WordPressDotCom clientId clientSecret = oauth2WordPressDotCom clientId clientSecret =
authOAuth2 pluginName oauth2 $ \manager token -> do authOAuth2 pluginName oauth2 $ \manager token -> do
(WpUser userId, userResponse) <- authGetProfile (WpUser userId, userResponse) <-
pluginName authGetProfile
manager pluginName
token manager
"https://public-api.wordpress.com/rest/v1/me/" 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 where
oauth2 = OAuth2 oauth2 =
{ oauth2ClientId = clientId OAuth2
, oauth2ClientSecret = Just clientSecret { oauth2ClientId = clientId
, oauth2AuthorizeEndpoint = , oauth2ClientSecret = Just clientSecret
"https://public-api.wordpress.com/oauth2/authorize" , oauth2AuthorizeEndpoint =
`withQuery` [scopeParam "," ["auth"]] "https://public-api.wordpress.com/oauth2/authorize"
, oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token" `withQuery` [scopeParam "," ["auth"]]
, oauth2RedirectUri = Nothing , oauth2TokenEndpoint = "https://public-api.wordpress.com/oauth2/token"
} , oauth2RedirectUri = Nothing
}

View File

@ -1,5 +1,6 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module URI.ByteString.ExtensionSpec module URI.ByteString.ExtensionSpec
( spec ( spec
) where ) where
@ -61,9 +62,8 @@ spec = do
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 it "handles a URI with an existing query" $ do
let let uriWithQuery =
uriWithQuery = [uri|http://example.com?foo=bar|] `withQuery` [("baz", "bat")]
[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|]
@ -71,9 +71,8 @@ spec = do
-- it's worthwhile to show that you don't (and can't) pre-sanitize when -- it's worthwhile to show that you don't (and can't) pre-sanitize when
-- using this function. -- using this function.
it "handles santization of the query" $ do it "handles santization of the query" $ do
let let uriWithQuery =
uriWithQuery = [uri|http://example.com|] `withQuery` [("foo", "bar baz")]
[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"