hoauth2-providers 0.1 → 0.2
raw patch · 17 files changed
+487/−259 lines, 17 filesdep +HsOpenSSLdep +cryptonitedep +jose-jwtdep ~hoauth2PVP ok
version bump matches the API change (PVP)
Dependencies added: HsOpenSSL, cryptonite, jose-jwt, time
Dependency ranges changed: hoauth2
API changes (from Hackage documentation)
- Network.OAuth2.Provider.AzureAD: [mail] :: AzureADUser -> Text
- Network.OAuth2.Provider.AzureAD: newtype AzureADUser
- Network.OAuth2.Provider.Dropbox: DropboxName :: Text -> DropboxName
- Network.OAuth2.Provider.Dropbox: instance Data.Aeson.Types.FromJSON.FromJSON Network.OAuth2.Provider.Dropbox.DropboxName
- Network.OAuth2.Provider.Dropbox: instance GHC.Generics.Generic Network.OAuth2.Provider.Dropbox.DropboxName
- Network.OAuth2.Provider.Dropbox: instance GHC.Show.Show Network.OAuth2.Provider.Dropbox.DropboxName
- Network.OAuth2.Provider.Dropbox: newtype DropboxName
+ Network.OAuth2.Provider.AzureAD: [email] :: AzureADUser -> Text
+ Network.OAuth2.Provider.AzureAD: [familyName] :: AzureADUser -> Text
+ Network.OAuth2.Provider.AzureAD: [givenName] :: AzureADUser -> Text
+ Network.OAuth2.Provider.AzureAD: [name] :: AzureADUser -> Text
+ Network.OAuth2.Provider.AzureAD: [sub] :: AzureADUser -> Text
+ Network.OAuth2.Provider.AzureAD: data AzureADUser
+ Network.OAuth2.Provider.Dropbox: DropboxUserName :: Text -> DropboxUserName
+ Network.OAuth2.Provider.Dropbox: instance Data.Aeson.Types.FromJSON.FromJSON Network.OAuth2.Provider.Dropbox.DropboxUserName
+ Network.OAuth2.Provider.Dropbox: instance GHC.Generics.Generic Network.OAuth2.Provider.Dropbox.DropboxUserName
+ Network.OAuth2.Provider.Dropbox: instance GHC.Show.Show Network.OAuth2.Provider.Dropbox.DropboxUserName
+ Network.OAuth2.Provider.Dropbox: newtype DropboxUserName
+ Network.OAuth2.Provider.Google: GoogleServiceAccountKey :: String -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> GoogleServiceAccountKey
+ Network.OAuth2.Provider.Google: [authProviderX509CertUrl] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [authUri] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [clientEmail] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [clientId] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [clientX509CertUrl] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [privateKeyId] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [privateKey] :: GoogleServiceAccountKey -> String
+ Network.OAuth2.Provider.Google: [projectId] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: [tokenUri] :: GoogleServiceAccountKey -> Text
+ Network.OAuth2.Provider.Google: data GoogleServiceAccountKey
+ Network.OAuth2.Provider.Google: defaultServiceAccountApp :: Jwt -> IdpApplication 'JwtBearer Google
+ Network.OAuth2.Provider.Google: instance Data.Aeson.Types.FromJSON.FromJSON Network.OAuth2.Provider.Google.GoogleServiceAccountKey
+ Network.OAuth2.Provider.Google: instance GHC.Generics.Generic Network.OAuth2.Provider.Google.GoogleServiceAccountKey
+ Network.OAuth2.Provider.Google: mkJwt :: PrivateKey -> Text -> Maybe Text -> Set Scope -> Idp Google -> IO (Either String Jwt)
+ Network.OAuth2.Provider.Google: readPemRsaKey :: String -> IO (Either String PrivateKey)
+ Network.OAuth2.Provider.Okta: mkOktaClientCredentialAppJwt :: Jwk -> ClientId -> Idp Okta -> IO (Either String Jwt)
+ Network.OAuth2.Provider.Twitter: Twitter :: Twitter
+ Network.OAuth2.Provider.Twitter: TwitterUser :: Text -> Text -> Text -> TwitterUser
+ Network.OAuth2.Provider.Twitter: TwitterUserResp :: TwitterUser -> TwitterUserResp
+ Network.OAuth2.Provider.Twitter: [id] :: TwitterUser -> Text
+ Network.OAuth2.Provider.Twitter: [name] :: TwitterUser -> Text
+ Network.OAuth2.Provider.Twitter: [twitterUserRespData] :: TwitterUserResp -> TwitterUser
+ Network.OAuth2.Provider.Twitter: [username] :: TwitterUser -> Text
+ Network.OAuth2.Provider.Twitter: data Twitter
+ Network.OAuth2.Provider.Twitter: data TwitterUser
+ Network.OAuth2.Provider.Twitter: defaultTwitterApp :: IdpApplication 'AuthorizationCode Twitter
+ Network.OAuth2.Provider.Twitter: defaultTwitterIdp :: Idp Twitter
+ Network.OAuth2.Provider.Twitter: instance Data.Aeson.Types.FromJSON.FromJSON Network.OAuth2.Provider.Twitter.TwitterUser
+ Network.OAuth2.Provider.Twitter: instance Data.Aeson.Types.FromJSON.FromJSON Network.OAuth2.Provider.Twitter.TwitterUserResp
+ Network.OAuth2.Provider.Twitter: instance GHC.Classes.Eq Network.OAuth2.Provider.Twitter.Twitter
+ Network.OAuth2.Provider.Twitter: instance GHC.Generics.Generic Network.OAuth2.Provider.Twitter.TwitterUser
+ Network.OAuth2.Provider.Twitter: instance GHC.Generics.Generic Network.OAuth2.Provider.Twitter.TwitterUserResp
+ Network.OAuth2.Provider.Twitter: instance GHC.Show.Show Network.OAuth2.Provider.Twitter.Twitter
+ Network.OAuth2.Provider.Twitter: instance GHC.Show.Show Network.OAuth2.Provider.Twitter.TwitterUser
+ Network.OAuth2.Provider.Twitter: instance GHC.Show.Show Network.OAuth2.Provider.Twitter.TwitterUserResp
+ Network.OAuth2.Provider.Twitter: newtype TwitterUserResp
+ Network.OAuth2.Provider.Utils: bsFromStrict :: ByteString -> ByteString
+ Network.OAuth2.Provider.Utils: bsToStrict :: ByteString -> ByteString
- Network.OAuth2.Provider.AzureAD: AzureADUser :: Text -> AzureADUser
+ Network.OAuth2.Provider.AzureAD: AzureADUser :: Text -> Text -> Text -> Text -> Text -> AzureADUser
- Network.OAuth2.Provider.Dropbox: DropboxUser :: Text -> DropboxName -> DropboxUser
+ Network.OAuth2.Provider.Dropbox: DropboxUser :: Text -> DropboxUserName -> DropboxUser
- Network.OAuth2.Provider.Dropbox: [displayName] :: DropboxName -> Text
+ Network.OAuth2.Provider.Dropbox: [displayName] :: DropboxUserName -> Text
- Network.OAuth2.Provider.Dropbox: [name] :: DropboxUser -> DropboxName
+ Network.OAuth2.Provider.Dropbox: [name] :: DropboxUser -> DropboxUserName
Files
- hoauth2-providers.cabal +10/−2
- src/Network/OAuth2/Provider/Auth0.hs +23/−22
- src/Network/OAuth2/Provider/AzureAD.hs +29/−15
- src/Network/OAuth2/Provider/Dropbox.hs +18/−18
- src/Network/OAuth2/Provider/Facebook.hs +17/−18
- src/Network/OAuth2/Provider/Fitbit.hs +16/−16
- src/Network/OAuth2/Provider/Github.hs +17/−17
- src/Network/OAuth2/Provider/Google.hs +125/−21
- src/Network/OAuth2/Provider/Linkedin.hs +16/−21
- src/Network/OAuth2/Provider/Okta.hs +50/−20
- src/Network/OAuth2/Provider/Slack.hs +15/−15
- src/Network/OAuth2/Provider/StackExchange.hs +21/−24
- src/Network/OAuth2/Provider/Twitter.hs +61/−0
- src/Network/OAuth2/Provider/Utils.hs +20/−0
- src/Network/OAuth2/Provider/Weibo.hs +18/−18
- src/Network/OAuth2/Provider/ZOHO.hs +18/−19
- src/Network/OIDC/WellKnown.hs +13/−13
hoauth2-providers.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: hoauth2-providers-version: 0.1+version: 0.2 synopsis: OAuth2 Identity Providers description: A list of Identity Providers base on hoauth2 Haskell OAuth2 binding@@ -35,6 +35,8 @@ TypeApplications TypeFamilies + -- other-modules:+ exposed-modules: Network.OAuth2.Provider.Auth0 Network.OAuth2.Provider.AzureAD@@ -47,6 +49,8 @@ Network.OAuth2.Provider.Okta Network.OAuth2.Provider.Slack Network.OAuth2.Provider.StackExchange+ Network.OAuth2.Provider.Twitter+ Network.OAuth2.Provider.Utils Network.OAuth2.Provider.Weibo Network.OAuth2.Provider.ZOHO Network.OIDC.WellKnown@@ -56,14 +60,18 @@ , base >=4.5 && <5 , bytestring >=0.9 && <0.12 , containers ^>=0.6+ , cryptonite >=0.30 && <0.31 , data-default ^>=0.7 , directory ^>=1.3- , hoauth2 ^>=2.6+ , hoauth2 >=2.7+ , HsOpenSSL >=0.11 && <0.12 , http-conduit >=2.1 && <2.4 , http-types >=0.11 && <0.13+ , jose-jwt >=0.9.4 && <0.10 , mtl , parsec >=3.1.11 && <3.2.0 , text >=0.11 && <1.3+ , time >=1.12 && <1.13 , transformers ^>=0.5 , unordered-containers >=0.2.5 , uri-bytestring >=0.2.3 && <0.4
src/Network/OAuth2/Provider/Auth0.hs view
@@ -8,6 +8,7 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} +-- | https://auth0.com/docs/api/authentication#authorize-application module Network.OAuth2.Provider.Auth0 where import Control.Monad.IO.Class@@ -30,26 +31,26 @@ defaultAuth0App :: Idp Auth0 -> IdpApplication 'AuthorizationCode Auth0 defaultAuth0App i = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["openid", "profile", "email", "offline_access"],- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-auth0-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = i+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["openid", "profile", "email", "offline_access"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-auth0-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = i } defaultAuth0Idp :: Idp Auth0 defaultAuth0Idp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Auth0),- -- https://auth0.com/docs/api/authentication#user-profile- idpUserInfoEndpoint = [uri|https://foo.auth0.com/userinfo|],- -- https://auth0.com/docs/api/authentication#authorization-code-flow- idpAuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|],- -- https://auth0.com/docs/api/authentication#authorization-code-flow44+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Auth0)+ , -- https://auth0.com/docs/api/authentication#user-profile+ idpUserInfoEndpoint = [uri|https://foo.auth0.com/userinfo|]+ , -- https://auth0.com/docs/api/authentication#authorization-code-flow+ idpAuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|]+ , -- https://auth0.com/docs/api/authentication#authorization-code-flow44 idpTokenEndpoint = [uri|https://foo.auth0.com/oauth/token|] } @@ -62,17 +63,17 @@ OpenIDConfigurationUris {..} <- fetchWellKnownUris domain pure ( defaultAuth0Idp- { idpUserInfoEndpoint = userinfoUri,- idpAuthorizeEndpoint = authorizationUri,- idpTokenEndpoint = tokenUri+ { idpUserInfoEndpoint = userinfoUri+ , idpAuthorizeEndpoint = authorizationUri+ , idpTokenEndpoint = tokenUri } ) --- | scopes: ["openid", "profile", "email"]+-- | https://auth0.com/docs/api/authentication#user-profile data Auth0User = Auth0User- { name :: Text,- email :: Text,- sub :: Text+ { name :: Text+ , email :: Text+ , sub :: Text } deriving (Show, Generic)
src/Network/OAuth2/Provider/AzureAD.hs view
@@ -15,29 +15,43 @@ type instance IdpUserInfo AzureAD = AzureADUser +-- create app at https://go.microsoft.com/fwlink/?linkid=2083908+--+-- also be aware to find the right client id.+-- see https://stackoverflow.com/a/70670961 defaultAzureADApp :: IdpApplication 'AuthorizationCode AzureAD defaultAzureADApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.fromList [("resource", "https://graph.microsoft.com")],- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-azure-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultAzureADIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["openid", "profile", "email"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-azure-app"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultAzureADIdp } +-- | https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration defaultAzureADIdp :: Idp AzureAD defaultAzureADIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo AzureAD),- idpUserInfoEndpoint = [uri|https://graph.microsoft.com/v1.0/me|],- idpAuthorizeEndpoint = [uri|https://login.windows.net/common/oauth2/authorize|],- idpTokenEndpoint = [uri|https://login.windows.net/common/oauth2/token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo AzureAD)+ , idpUserInfoEndpoint = [uri|https://graph.microsoft.com/oidc/userinfo|]+ , idpAuthorizeEndpoint = [uri|https://login.microsoftonline.com/common/oauth2/v2.0/authorize|]+ , idpTokenEndpoint = [uri|https://login.microsoftonline.com/common/oauth2/v2.0/token|] } -newtype AzureADUser = AzureADUser {mail :: Text} deriving (Show, Generic)+-- | https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo+data AzureADUser = AzureADUser+ { sub :: Text+ , email :: Text+ , familyName :: Text+ , givenName :: Text+ , name :: Text+ }+ deriving (Show, Generic) -instance FromJSON AzureADUser+instance FromJSON AzureADUser where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
src/Network/OAuth2/Provider/Dropbox.hs view
@@ -22,36 +22,36 @@ defaultDropboxApp :: IdpApplication 'AuthorizationCode Dropbox defaultDropboxApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-dropbox-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultDropboxIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-dropbox-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultDropboxIdp } defaultDropboxIdp :: Idp Dropbox defaultDropboxIdp = Idp- { idpFetchUserInfo = \mgr at url -> authPostJSON @(IdpUserInfo Dropbox) mgr at url [],- idpAuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|],- idpTokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|],- idpUserInfoEndpoint = [uri|https://api.dropboxapi.com/2/users/get_current_account|]+ { idpFetchUserInfo = \mgr at url -> authPostJSON @(IdpUserInfo Dropbox) mgr at url []+ , idpAuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|]+ , idpTokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|]+ , idpUserInfoEndpoint = [uri|https://api.dropboxapi.com/2/users/get_current_account|] } -newtype DropboxName = DropboxName {displayName :: Text}+newtype DropboxUserName = DropboxUserName {displayName :: Text} deriving (Show, Generic) data DropboxUser = DropboxUser- { email :: Text,- name :: DropboxName+ { email :: Text+ , name :: DropboxUserName } deriving (Show, Generic) -instance FromJSON DropboxName where+instance FromJSON DropboxUserName where parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'} -instance FromJSON DropboxUser +instance FromJSON DropboxUser
src/Network/OAuth2/Provider/Facebook.hs view
@@ -15,7 +15,6 @@ import Network.OAuth2.Experiment import URI.ByteString.QQ --- | TODO: rename to Meta data Facebook = Facebook deriving (Eq, Show) type instance IdpUserInfo Facebook = FacebookUser@@ -23,31 +22,31 @@ defaultFacebookApp :: IdpApplication 'AuthorizationCode Facebook defaultFacebookApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["user_about_me", "email"],- idpAppAuthorizeExtraParams = Map.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-facebook-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretPost,- idp = defaultFacebookIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["user_about_me", "email"]+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-facebook-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretPost+ , idp = defaultFacebookIdp } defaultFacebookIdp :: Idp Facebook defaultFacebookIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Facebook),- idpUserInfoEndpoint = [uri|https://graph.facebook.com/me?fields=id,name,email|],- idpAuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|],- idpTokenEndpoint = [uri|https://graph.facebook.com/v2.3/oauth/access_token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Facebook)+ , idpUserInfoEndpoint = [uri|https://graph.facebook.com/me?fields=id,name,email|]+ , idpAuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|]+ , idpTokenEndpoint = [uri|https://graph.facebook.com/v2.3/oauth/access_token|] } data FacebookUser = FacebookUser- { id :: Text,- name :: Text,- email :: Text+ { id :: Text+ , name :: Text+ , email :: Text } deriving (Show, Generic) -instance FromJSON FacebookUser +instance FromJSON FacebookUser
src/Network/OAuth2/Provider/Fitbit.hs view
@@ -22,30 +22,30 @@ defaultFitbitApp :: IdpApplication 'AuthorizationCode Fitbit defaultFitbitApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeExtraParams = Map.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-fitbit-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultFitbitIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.empty+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-fitbit-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultFitbitIdp } defaultFitbitIdp :: Idp Fitbit defaultFitbitIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Fitbit),- idpUserInfoEndpoint = [uri|https://api.fitbit.com/1/user/-/profile.json|],- idpAuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|],- idpTokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Fitbit)+ , idpUserInfoEndpoint = [uri|https://api.fitbit.com/1/user/-/profile.json|]+ , idpAuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|]+ , idpTokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|] } data FitbitUser = FitbitUser- { userId :: Text,- userName :: Text,- userAge :: Int+ { userId :: Text+ , userName :: Text+ , userAge :: Int } deriving (Show, Eq)
src/Network/OAuth2/Provider/Github.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} +-- | https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps module Network.OAuth2.Provider.Github where import Data.Aeson@@ -15,7 +16,6 @@ import Network.OAuth2.Experiment import URI.ByteString.QQ --- | http://developer.github.com/v3/oauth/ data Github = Github deriving (Eq, Show) type instance IdpUserInfo Github = GithubUser@@ -23,30 +23,30 @@ defaultGithubApp :: IdpApplication 'AuthorizationCode Github defaultGithubApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-github-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultGithubIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-github-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultGithubIdp } defaultGithubIdp :: Idp Github defaultGithubIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Github),- idpUserInfoEndpoint = [uri|https://api.github.com/user|],- idpAuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|],- idpTokenEndpoint = [uri|https://github.com/login/oauth/access_token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Github)+ , idpUserInfoEndpoint = [uri|https://api.github.com/user|]+ , idpAuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|]+ , idpTokenEndpoint = [uri|https://github.com/login/oauth/access_token|] } data GithubUser = GithubUser- { name :: Text,- id :: Integer+ { name :: Text+ , id :: Integer } deriving (Show, Generic) -instance FromJSON GithubUser +instance FromJSON GithubUser
src/Network/OAuth2/Provider/Google.hs view
@@ -9,13 +9,30 @@ module Network.OAuth2.Provider.Google where +import Crypto.PubKey.RSA.Types import Data.Aeson+import Data.Aeson qualified as Aeson+import Data.Bifunctor import Data.Map.Strict qualified as Map+import Data.Maybe import Data.Set qualified as Set+import Data.Text qualified as T import Data.Text.Lazy (Text)+import Data.Text.Lazy qualified as TL+import Data.Time import GHC.Generics+import Jose.Jwa+import Jose.Jws+import Jose.Jwt import Network.OAuth.OAuth2 import Network.OAuth2.Experiment+import Network.OAuth2.Provider.Utils+import OpenSSL.EVP.PKey (toKeyPair)+import OpenSSL.PEM (+ PemPasswordSupply (PwNone),+ readPrivateKey,+ )+import OpenSSL.RSA import URI.ByteString.QQ {-@@ -26,39 +43,126 @@ type instance IdpUserInfo Google = GoogleUser +-- * Authorization Code flow+ defaultGoogleApp :: IdpApplication 'AuthorizationCode Google defaultGoogleApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope =+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList- [ "https://www.googleapis.com/auth/userinfo.email",- "https://www.googleapis.com/auth/userinfo.profile"- ],- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-google-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultGoogleIdp+ [ "https://www.googleapis.com/auth/userinfo.email"+ , "https://www.googleapis.com/auth/userinfo.profile"+ ]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-google-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultGoogleIdp } +-- * Service Account++-- | Service account key (in JSON format) that download from google+data GoogleServiceAccountKey = GoogleServiceAccountKey+ { privateKey :: String+ , clientEmail :: Text+ , projectId :: Text+ , privateKeyId :: Text+ , clientId :: Text+ , authUri :: Text+ , tokenUri :: Text+ , authProviderX509CertUrl :: Text+ , clientX509CertUrl :: Text+ }+ deriving (Generic)++instance FromJSON GoogleServiceAccountKey where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++-- * Service Account++mkJwt ::+ PrivateKey ->+ -- | Private key+ Text ->+ -- | Issuer+ Maybe Text ->+ -- | impersonate user+ Set.Set Scope ->+ -- | Scope+ Idp Google ->+ IO (Either String Jwt)+mkJwt privateKey iss muser scopes idp = do+ now <- getCurrentTime+ let payload =+ bsToStrict $+ Aeson.encode $+ Aeson.object $+ [ "iss" .= iss+ , "scope" .= T.intercalate " " (map (TL.toStrict . unScope) $ Set.toList scopes)+ , "aud" .= idpTokenEndpoint idp+ , "exp" .= tToSeconds (addUTCTime (secondsToNominalDiffTime 300) now) -- 5 minutes expiration time+ , "iat" .= tToSeconds now+ ]+ ++ maybe [] (\a -> ["sub" .= a]) muser+ first show <$> rsaEncode RS256 privateKey payload+ where+ tToSeconds = formatTime defaultTimeLocale "%s"++-- | Read private RSA Key in PEM format+readPemRsaKey ::+ -- | PEM content+ String ->+ IO (Either String PrivateKey)+readPemRsaKey pemStr = do+ somePair <- readPrivateKey pemStr PwNone+ pure $ case (toKeyPair somePair :: Maybe RSAKeyPair) of+ Just k ->+ Right $+ PrivateKey+ { private_pub =+ PublicKey+ { public_size = rsaSize k+ , public_n = rsaN k+ , public_e = rsaE k+ }+ , private_d = rsaD k+ , private_p = rsaP k+ , private_q = rsaQ k+ , private_dP = fromMaybe 0 (rsaDMP1 k)+ , private_dQ = fromMaybe 0 (rsaDMQ1 k)+ , private_qinv = fromMaybe 0 (rsaIQMP k)+ }+ Nothing -> Left "unable to parse PEM to RSA key"++defaultServiceAccountApp :: Jwt -> IdpApplication 'JwtBearer Google+defaultServiceAccountApp jwt =+ JwtBearerIdpApplication+ { idpAppName = "google-sa-app"+ , idpAppJwt = unJwt jwt+ , idp = defaultGoogleIdp+ }++-- * IDP+ defaultGoogleIdp :: Idp Google defaultGoogleIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Google),- idpAuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/v2/auth|],- idpTokenEndpoint = [uri|https://oauth2.googleapis.com/token|],- idpUserInfoEndpoint = [uri|https://www.googleapis.com/oauth2/v2/userinfo|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Google)+ , idpAuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/v2/auth|]+ , idpTokenEndpoint = [uri|https://oauth2.googleapis.com/token|]+ , idpUserInfoEndpoint = [uri|https://www.googleapis.com/oauth2/v2/userinfo|] } +-- requires scope "https://www.googleapis.com/auth/userinfo.profile" to obtain "name".+-- requires scopes "https://www.googleapis.com/auth/userinfo.email" to obtain "email". data GoogleUser = GoogleUser- { -- | "scope": "https://www.googleapis.com/auth/userinfo.profile"]- name :: Text,- id :: Text,- -- | "scopes": "https://www.googleapis.com/auth/userinfo.email",- email :: Text+ { name :: Text+ , id :: Text+ , email :: Text } deriving (Show, Generic)
src/Network/OAuth2/Provider/Linkedin.hs view
@@ -6,11 +6,6 @@ module Network.OAuth2.Provider.Linkedin where -{--https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?context=linkedin%2Fcontext&tabs=HTTPS-module Network.OAuth2.Provider.Linkedin where--}- import Data.Aeson import Data.Map.Strict qualified as Map import Data.Set qualified as Set@@ -27,30 +22,30 @@ defaultLinkedinApp :: IdpApplication 'AuthorizationCode Linkedin defaultLinkedinApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["r_liteprofile"],- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-linkedin-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretPost,- idp = defaultLinkedinIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["r_liteprofile"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-linkedin-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretPost+ , idp = defaultLinkedinIdp } defaultLinkedinIdp :: Idp Linkedin defaultLinkedinIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Linkedin),- idpUserInfoEndpoint = [uri|https://api.linkedin.com/v2/me|],- idpAuthorizeEndpoint = [uri|https://www.linkedin.com/oauth/v2/authorization|],- idpTokenEndpoint = [uri|https://www.linkedin.com/oauth/v2/accessToken|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Linkedin)+ , idpUserInfoEndpoint = [uri|https://api.linkedin.com/v2/me|]+ , idpAuthorizeEndpoint = [uri|https://www.linkedin.com/oauth/v2/authorization|]+ , idpTokenEndpoint = [uri|https://www.linkedin.com/oauth/v2/accessToken|] } data LinkedinUser = LinkedinUser- { localizedFirstName :: Text,- localizedLastName :: Text+ { localizedFirstName :: Text+ , localizedLastName :: Text } deriving (Show, Generic, Eq) -instance FromJSON LinkedinUser +instance FromJSON LinkedinUser
src/Network/OAuth2/Provider/Okta.hs view
@@ -10,12 +10,20 @@ import Control.Monad.IO.Class import Control.Monad.Trans.Except import Data.Aeson+import Data.Aeson qualified as Aeson+import Data.Bifunctor import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text.Lazy (Text)+import Data.Time import GHC.Generics+import Jose.Jwa+import Jose.Jwk+import Jose.Jws+import Jose.Jwt import Network.OAuth.OAuth2 import Network.OAuth2.Experiment+import Network.OAuth2.Provider.Utils import Network.OIDC.WellKnown import URI.ByteString.QQ @@ -26,25 +34,25 @@ defaultOktaApp :: Idp Okta -> IdpApplication 'AuthorizationCode Okta defaultOktaApp i = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["openid", "profile", "email"],- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-okta-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = i+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["openid", "profile", "email"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-okta-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = i } defaultOktaIdp :: Idp Okta defaultOktaIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Okta),- idpUserInfoEndpoint = [uri|https://foo.okta.com/oauth2/v1/userinfo|],- idpAuthorizeEndpoint =- [uri|https://foo.okta.com/oauth2/v1/authorize|],- idpTokenEndpoint =+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Okta)+ , idpUserInfoEndpoint = [uri|https://foo.okta.com/oauth2/v1/userinfo|]+ , idpAuthorizeEndpoint =+ [uri|https://foo.okta.com/oauth2/v1/authorize|]+ , idpTokenEndpoint = [uri|https://foo.okta.com/oauth2/v1/token|] } @@ -57,18 +65,40 @@ OpenIDConfigurationUris {..} <- fetchWellKnownUris domain pure ( defaultOktaIdp- { idpUserInfoEndpoint = userinfoUri,- idpAuthorizeEndpoint = authorizationUri,- idpTokenEndpoint = tokenUri+ { idpUserInfoEndpoint = userinfoUri+ , idpAuthorizeEndpoint = authorizationUri+ , idpTokenEndpoint = tokenUri } ) --- | https://developer.okta.com/docs/reference/api/oidc/#request-parameters+mkOktaClientCredentialAppJwt ::+ Jwk ->+ ClientId ->+ Idp Okta ->+ IO (Either String Jwt)+mkOktaClientCredentialAppJwt jwk cid idp = do+ now <- getCurrentTime+ let cidStr = unClientId cid+ let payload =+ bsToStrict $+ Aeson.encode $+ Aeson.object+ [ "iss" .= cidStr+ , "sub" .= cidStr+ , "aud" .= idpTokenEndpoint idp+ , "exp" .= tToSeconds (addUTCTime (secondsToNominalDiffTime 300) now) -- 5 minutes expiration time+ , "iat" .= tToSeconds now+ ]+ first show <$> jwkEncode RS256 jwk (Claims payload)+ where+ tToSeconds = formatTime defaultTimeLocale "%s"++-- https://developer.okta.com/docs/reference/api/oidc/#request-parameters -- Okta Org AS doesn't support consent -- Okta Custom AS does support consent via config (what scope shall prompt consent) data OktaUser = OktaUser- { name :: Text,- preferredUsername :: Text+ { name :: Text+ , preferredUsername :: Text } deriving (Show, Generic)
src/Network/OAuth2/Provider/Slack.hs view
@@ -22,15 +22,15 @@ defaultSlackApp :: IdpApplication 'AuthorizationCode Slack defaultSlackApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["openid", "profile"],- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idpAppName = "default-slack-App",- idp = defaultSlackIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["openid", "profile"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idpAppName = "default-slack-App"+ , idp = defaultSlackIdp } -- https://api.slack.com/authentication/sign-in-with-slack@@ -38,15 +38,15 @@ defaultSlackIdp :: Idp Slack defaultSlackIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo Slack),- idpUserInfoEndpoint = [uri|https://slack.com/api/openid.connect.userInfo|],- idpAuthorizeEndpoint = [uri|https://slack.com/openid/connect/authorize|],- idpTokenEndpoint = [uri|https://slack.com/api/openid.connect.token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Slack)+ , idpUserInfoEndpoint = [uri|https://slack.com/api/openid.connect.userInfo|]+ , idpAuthorizeEndpoint = [uri|https://slack.com/openid/connect/authorize|]+ , idpTokenEndpoint = [uri|https://slack.com/api/openid.connect.token|] } data SlackUser = SlackUser- { name :: Text,- email :: Text+ { name :: Text+ , email :: Text } deriving (Show, Generic)
src/Network/OAuth2/Provider/StackExchange.hs view
@@ -4,9 +4,6 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TypeFamilies #-} -{-- NOTES: stackexchange API spec and its document just sucks!--} module Network.OAuth2.Provider.StackExchange where import Data.Aeson@@ -32,45 +29,45 @@ defaultStackExchangeApp :: IdpApplication 'AuthorizationCode StackExchange defaultStackExchangeApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppName = "default-stackexchange-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretPost,- idp = defaultStackexchangeIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-stackexchange-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretPost+ , idp = defaultStackexchangeIdp } defaultStackexchangeIdp :: Idp StackExchange defaultStackexchangeIdp = Idp- { idpFetchUserInfo = authGetJSONWithAuthMethod @_ @(IdpUserInfo StackExchange) AuthInRequestQuery,- -- Only StackExchange has such specical app key which has to be append in userinfo uri.+ { idpFetchUserInfo = authGetJSONWithAuthMethod @_ @(IdpUserInfo StackExchange) AuthInRequestQuery+ , -- Only StackExchange has such specical app key which has to be append in userinfo uri. -- I feel it's not worth to invent a way to read from config -- file which would break the generic of Idp data type. -- Until discover a easier way, hard code for now. idpUserInfoEndpoint = appendStackExchangeAppKey [uri|https://api.stackexchange.com/2.2/me?site=stackoverflow|]- stackexchangeAppKey,- idpAuthorizeEndpoint = [uri|https://stackexchange.com/oauth|],- idpTokenEndpoint = [uri|https://stackexchange.com/oauth/access_token|]+ stackexchangeAppKey+ , idpAuthorizeEndpoint = [uri|https://stackexchange.com/oauth|]+ , idpTokenEndpoint = [uri|https://stackexchange.com/oauth/access_token|] } data StackExchangeResp = StackExchangeResp- { hasMore :: Bool,- quotaMax :: Integer,- quotaRemaining :: Integer,- items :: [StackExchangeUser]+ { hasMore :: Bool+ , quotaMax :: Integer+ , quotaRemaining :: Integer+ , items :: [StackExchangeUser] } deriving (Show, Generic) data StackExchangeUser = StackExchangeUser- { userId :: Integer,- displayName :: Text,- profileImage :: Text+ { userId :: Integer+ , displayName :: Text+ , profileImage :: Text } deriving (Show, Generic)
+ src/Network/OAuth2/Provider/Twitter.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++-- | https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code+module Network.OAuth2.Provider.Twitter where++import Data.Aeson+import Data.Char (toLower)+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text.Lazy (Text)+import GHC.Generics+import Network.OAuth.OAuth2+import Network.OAuth2.Experiment+import URI.ByteString.QQ++data Twitter = Twitter deriving (Eq, Show)++type instance IdpUserInfo Twitter = TwitterUserResp++defaultTwitterApp :: IdpApplication 'AuthorizationCode Twitter+defaultTwitterApp =+ AuthorizationCodeIdpApplication+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["tweet.read", "users.read"]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppName = "default-twitter-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultTwitterIdp+ }++defaultTwitterIdp :: Idp Twitter+defaultTwitterIdp =+ Idp+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo Twitter)+ , idpUserInfoEndpoint = [uri|https://api.twitter.com/2/users/me|]+ , idpAuthorizeEndpoint = [uri|https://twitter.com/i/oauth2/authorize|]+ , idpTokenEndpoint = [uri|https://api.twitter.com/2/oauth2/token|]+ }++data TwitterUser = TwitterUser+ { name :: Text+ , id :: Text+ , username :: Text+ }+ deriving (Show, Generic)++newtype TwitterUserResp = TwitterUserResp {twitterUserRespData :: TwitterUser}+ deriving (Show, Generic)++instance FromJSON TwitterUserResp where+ -- 15 = length "twitterUserResp"+ parseJSON = genericParseJSON (defaultOptions {fieldLabelModifier = map toLower . drop 15})++instance FromJSON TwitterUser
+ src/Network/OAuth2/Provider/Utils.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE CPP #-}++module Network.OAuth2.Provider.Utils where++import Data.ByteString qualified as BS+import Data.ByteString.Lazy qualified as BSL++bsToStrict :: BSL.ByteString -> BS.ByteString+#if MIN_VERSION_bytestring(0,11,0)+bsToStrict = BS.toStrict+#else+bsToStrict = BSL.toStrict+#endif++bsFromStrict :: BS.ByteString -> BSL.ByteString+#if MIN_VERSION_bytestring(0,11,0)+bsFromStrict = BS.fromStrict+#else+bsFromStrict = BSL.fromStrict+#endif
src/Network/OAuth2/Provider/Weibo.hs view
@@ -18,38 +18,38 @@ defaultWeiboApp :: IdpApplication 'AuthorizationCode Weibo defaultWeiboApp = AuthorizationCodeIdpApplication- { idpAppName = "default-weibo-App",- idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.empty,- idpAppAuthorizeState = "CHANGE_ME",- idpAppAuthorizeExtraParams = Map.empty,- idpAppRedirectUri = [uri|http://localhost|],- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultWeiboIdp+ { idpAppName = "default-weibo-App"+ , idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.empty+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppAuthorizeExtraParams = Map.empty+ , idpAppRedirectUri = [uri|http://localhost|]+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultWeiboIdp } defaultWeiboIdp :: Idp Weibo defaultWeiboIdp = Idp- { idpFetchUserInfo = authGetJSONWithAuthMethod @_ @(IdpUserInfo Weibo) AuthInRequestQuery,- idpUserInfoEndpoint = [uri|https://api.weibo.com/2/account/get_uid.json|],- idpAuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|],- idpTokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|]+ { idpFetchUserInfo = authGetJSONWithAuthMethod @_ @(IdpUserInfo Weibo) AuthInRequestQuery+ , idpUserInfoEndpoint = [uri|https://api.weibo.com/2/account/get_uid.json|]+ , idpAuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|]+ , idpTokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|] } --- | UserInfor API: http://open.weibo.com/wiki/2/users/show+-- | http://open.weibo.com/wiki/2/users/show data WeiboUser = WeiboUser- { id :: Integer,- name :: Text,- screenName :: Text+ { id :: Integer+ , name :: Text+ , screenName :: Text } deriving (Show, Generic) newtype WeiboUID = WeiboUID {uid :: Integer} deriving (Show, Generic) -instance FromJSON WeiboUID +instance FromJSON WeiboUID instance FromJSON WeiboUser where parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
src/Network/OAuth2/Provider/ZOHO.hs view
@@ -15,9 +15,6 @@ import Network.OAuth2.Experiment import URI.ByteString.QQ --- `oauth/user/info` url does not work and find answer from--- https://help.zoho.com/portal/community/topic/oauth2-api-better-document-oauth-user-info- data ZOHO = ZOHO deriving (Eq, Show) type instance IdpUserInfo ZOHO = ZOHOUserResp@@ -25,36 +22,38 @@ defaultZohoApp :: IdpApplication 'AuthorizationCode ZOHO defaultZohoApp = AuthorizationCodeIdpApplication- { idpAppClientId = "",- idpAppClientSecret = "",- idpAppScope = Set.fromList ["ZohoCRM.users.READ"],- idpAppAuthorizeExtraParams = Map.fromList [("access_type", "offline"), ("prompt", "consent")],- idpAppAuthorizeState = "CHANGE_ME",- idpAppRedirectUri = [uri|http://localhost/oauth2/callback|],- idpAppName = "default-zoho-App",- idpAppTokenRequestAuthenticationMethod = ClientSecretBasic,- idp = defaultZohoIdp+ { idpAppClientId = ""+ , idpAppClientSecret = ""+ , idpAppScope = Set.fromList ["ZohoCRM.users.READ"]+ , idpAppAuthorizeExtraParams = Map.fromList [("access_type", "offline"), ("prompt", "consent")]+ , idpAppAuthorizeState = "CHANGE_ME"+ , idpAppRedirectUri = [uri|http://localhost/oauth2/callback|]+ , idpAppName = "default-zoho-App"+ , idpAppTokenRequestAuthenticationMethod = ClientSecretBasic+ , idp = defaultZohoIdp } defaultZohoIdp :: Idp ZOHO defaultZohoIdp = Idp- { idpFetchUserInfo = authGetJSON @(IdpUserInfo ZOHO),- idpUserInfoEndpoint = [uri|https://www.zohoapis.com/crm/v2/users|],- idpAuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|],- idpTokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|]+ { idpFetchUserInfo = authGetJSON @(IdpUserInfo ZOHO)+ , idpUserInfoEndpoint = [uri|https://www.zohoapis.com/crm/v2/users|]+ , idpAuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|]+ , idpTokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|] } +-- `oauth/user/info` url does not work and find answer from+-- https://help.zoho.com/portal/community/topic/oauth2-api-better-document-oauth-user-info data ZOHOUser = ZOHOUser- { email :: Text,- fullName :: Text+ { email :: Text+ , fullName :: Text } deriving (Show, Generic) newtype ZOHOUserResp = ZOHOUserResp {users :: [ZOHOUser]} deriving (Show, Generic) -instance FromJSON ZOHOUserResp +instance FromJSON ZOHOUserResp instance FromJSON ZOHOUser where parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
src/Network/OIDC/WellKnown.hs view
@@ -18,19 +18,19 @@ -- | Slim OpenID Configuration -- TODO: could add more fields to be complete. data OpenIDConfiguration = OpenIDConfiguration- { issuer :: Text,- authorizationEndpoint :: Text,- tokenEndpoint :: Text,- userinfoEndpoint :: Text,- jwksUri :: Text+ { issuer :: Text+ , authorizationEndpoint :: Text+ , tokenEndpoint :: Text+ , userinfoEndpoint :: Text+ , jwksUri :: Text } deriving (Generic) data OpenIDConfigurationUris = OpenIDConfigurationUris- { authorizationUri :: URI,- tokenUri :: URI,- userinfoUri :: URI,- jwksUri :: URI+ { authorizationUri :: URI+ , tokenUri :: URI+ , userinfoUri :: URI+ , jwksUri :: URI } deriving (Generic) @@ -61,10 +61,10 @@ jwks <- ExceptT $ pure (parseURI strictURIParserOptions $ BSL.toStrict $ TL.encodeUtf8 jwksUri) pure OpenIDConfigurationUris- { authorizationUri = ae,- tokenUri = te,- userinfoUri = ue,- jwksUri = jwks+ { authorizationUri = ae+ , tokenUri = te+ , userinfoUri = ue+ , jwksUri = jwks } handleWellKnownResponse :: Response ByteString -> Either Text OpenIDConfiguration