hoauth2-providers (empty) → 0.1
raw patch · 16 files changed
+962/−0 lines, 16 filesdep +aesondep +basedep +bytestring
Dependencies added: aeson, base, bytestring, containers, data-default, directory, hoauth2, http-conduit, http-types, mtl, parsec, text, transformers, unordered-containers, uri-bytestring
Files
- LICENSE +21/−0
- hoauth2-providers.cabal +73/−0
- src/Network/OAuth2/Provider/Auth0.hs +79/−0
- src/Network/OAuth2/Provider/AzureAD.hs +43/−0
- src/Network/OAuth2/Provider/Dropbox.hs +57/−0
- src/Network/OAuth2/Provider/Facebook.hs +53/−0
- src/Network/OAuth2/Provider/Fitbit.hs +58/−0
- src/Network/OAuth2/Provider/Github.hs +52/−0
- src/Network/OAuth2/Provider/Google.hs +65/−0
- src/Network/OAuth2/Provider/Linkedin.hs +56/−0
- src/Network/OAuth2/Provider/Okta.hs +77/−0
- src/Network/OAuth2/Provider/Slack.hs +53/−0
- src/Network/OAuth2/Provider/StackExchange.hs +84/−0
- src/Network/OAuth2/Provider/Weibo.hs +55/−0
- src/Network/OAuth2/Provider/ZOHO.hs +60/−0
- src/Network/OIDC/WellKnown.hs +76/−0
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2022 Haisheng Wu++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ hoauth2-providers.cabal view
@@ -0,0 +1,73 @@+cabal-version: 2.4+name: hoauth2-providers+version: 0.1+synopsis: OAuth2 Identity Providers+description:+ A list of Identity Providers base on hoauth2 Haskell OAuth2 binding++homepage: https://github.com/freizl/hoauth2+license: MIT+license-file: LICENSE+author: Haisheng Wu+maintainer: Haisheng Wu <freizl@gmail.com>+copyright: Haisheng Wu+category: Network+build-type: Simple+stability: Beta+tested-with: GHC <=9.2.2++-- extra-source-files:++source-repository head+ type: git+ location: git://github.com/freizl/hoauth2.git++library+ hs-source-dirs: src+ default-language: Haskell2010+ default-extensions:+ DataKinds+ DeriveGeneric+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ OverloadedStrings+ RecordWildCards+ TypeApplications+ TypeFamilies++ exposed-modules:+ Network.OAuth2.Provider.Auth0+ Network.OAuth2.Provider.AzureAD+ Network.OAuth2.Provider.Dropbox+ Network.OAuth2.Provider.Facebook+ Network.OAuth2.Provider.Fitbit+ Network.OAuth2.Provider.Github+ Network.OAuth2.Provider.Google+ Network.OAuth2.Provider.Linkedin+ Network.OAuth2.Provider.Okta+ Network.OAuth2.Provider.Slack+ Network.OAuth2.Provider.StackExchange+ Network.OAuth2.Provider.Weibo+ Network.OAuth2.Provider.ZOHO+ Network.OIDC.WellKnown++ build-depends:+ , aeson >=2.0 && <2.2+ , base >=4.5 && <5+ , bytestring >=0.9 && <0.12+ , containers ^>=0.6+ , data-default ^>=0.7+ , directory ^>=1.3+ , hoauth2 ^>=2.6+ , http-conduit >=2.1 && <2.4+ , http-types >=0.11 && <0.13+ , mtl+ , parsec >=3.1.11 && <3.2.0+ , text >=0.11 && <1.3+ , transformers ^>=0.5+ , unordered-containers >=0.2.5+ , uri-bytestring >=0.2.3 && <0.4++ ghc-options:+ -Wall -Wtabs -Wno-unused-do-bind -Wunused-packages -Wpartial-fields+ -Wwarnings-deprecations
+ src/Network/OAuth2/Provider/Auth0.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Auth0 where++import Control.Monad.IO.Class+import Control.Monad.Trans.Except+import Data.Aeson+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 Network.OIDC.WellKnown+import URI.ByteString.QQ++data Auth0 = Auth0+ deriving (Show, Eq)++type instance IdpUserInfo Auth0 = Auth0User++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+ }++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+ idpTokenEndpoint = [uri|https://foo.auth0.com/oauth/token|]+ }++mkAuth0Idp ::+ MonadIO m =>+ -- | Full domain with no http protocol. e.g. @foo.auth0.com@+ Text ->+ ExceptT Text m (Idp Auth0)+mkAuth0Idp domain = do+ OpenIDConfigurationUris {..} <- fetchWellKnownUris domain+ pure+ ( defaultAuth0Idp+ { idpUserInfoEndpoint = userinfoUri,+ idpAuthorizeEndpoint = authorizationUri,+ idpTokenEndpoint = tokenUri+ }+ )++-- | scopes: ["openid", "profile", "email"]+data Auth0User = Auth0User+ { name :: Text,+ email :: Text,+ sub :: Text+ }+ deriving (Show, Generic)++instance FromJSON Auth0User
+ src/Network/OAuth2/Provider/AzureAD.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE QuasiQuotes #-}++module Network.OAuth2.Provider.AzureAD where++import Data.Aeson+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 AzureAD = AzureAD deriving (Eq, Show)++type instance IdpUserInfo AzureAD = AzureADUser++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+ }++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|]+ }++newtype AzureADUser = AzureADUser {mail :: Text} deriving (Show, Generic)++instance FromJSON AzureADUser
+ src/Network/OAuth2/Provider/Dropbox.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Dropbox where++import Data.Aeson+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 Dropbox = Dropbox deriving (Eq, Show)++type instance IdpUserInfo Dropbox = DropboxUser++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+ }++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|]+ }++newtype DropboxName = DropboxName {displayName :: Text}+ deriving (Show, Generic)++data DropboxUser = DropboxUser+ { email :: Text,+ name :: DropboxName+ }+ deriving (Show, Generic)++instance FromJSON DropboxName where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++instance FromJSON DropboxUser
+ src/Network/OAuth2/Provider/Facebook.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Facebook where++import Data.Aeson+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++-- | TODO: rename to Meta+data Facebook = Facebook deriving (Eq, Show)++type instance IdpUserInfo Facebook = FacebookUser++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+ }++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|]+ }++data FacebookUser = FacebookUser+ { id :: Text,+ name :: Text,+ email :: Text+ }+ deriving (Show, Generic)++instance FromJSON FacebookUser
+ src/Network/OAuth2/Provider/Fitbit.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Fitbit where++import Control.Monad (mzero)+import Data.Aeson+import Data.Map.Strict qualified as Map+import Data.Set qualified as Set+import Data.Text.Lazy (Text)+import Network.OAuth.OAuth2+import Network.OAuth2.Experiment+import URI.ByteString.QQ++data Fitbit = Fitbit deriving (Eq, Show)++type instance IdpUserInfo Fitbit = FitbitUser++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+ }++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|]+ }++data FitbitUser = FitbitUser+ { userId :: Text,+ userName :: Text,+ userAge :: Int+ }+ deriving (Show, Eq)++instance FromJSON FitbitUser where+ parseJSON (Object o) =+ FitbitUser+ <$> ((o .: "user") >>= (.: "encodedId"))+ <*> ((o .: "user") >>= (.: "fullName"))+ <*> ((o .: "user") >>= (.: "age"))+ parseJSON _ = mzero
+ src/Network/OAuth2/Provider/Github.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Github where++import Data.Aeson+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++-- | http://developer.github.com/v3/oauth/+data Github = Github deriving (Eq, Show)++type instance IdpUserInfo Github = GithubUser++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+ }++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|]+ }++data GithubUser = GithubUser+ { name :: Text,+ id :: Integer+ }+ deriving (Show, Generic)++instance FromJSON GithubUser
+ src/Network/OAuth2/Provider/Google.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE InstanceSigs #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Google where++import Data.Aeson+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++{-+To test at google playground, set redirect uri to "https://developers.google.com/oauthplayground"+-}++data Google = Google deriving (Eq, Show)++type instance IdpUserInfo Google = GoogleUser++defaultGoogleApp :: IdpApplication 'AuthorizationCode Google+defaultGoogleApp =+ AuthorizationCodeIdpApplication+ { 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+ }++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|]+ }++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+ }+ deriving (Show, Generic)++instance FromJSON GoogleUser
+ src/Network/OAuth2/Provider/Linkedin.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++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+import Data.Text.Lazy (Text)+import GHC.Generics+import Network.OAuth.OAuth2+import Network.OAuth2.Experiment+import URI.ByteString.QQ++data Linkedin = Linkedin deriving (Eq, Show)++type instance IdpUserInfo Linkedin = LinkedinUser++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+ }++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|]+ }++data LinkedinUser = LinkedinUser+ { localizedFirstName :: Text,+ localizedLastName :: Text+ }+ deriving (Show, Generic, Eq)++instance FromJSON LinkedinUser
+ src/Network/OAuth2/Provider/Okta.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Okta where++import Control.Monad.IO.Class+import Control.Monad.Trans.Except+import Data.Aeson+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 Network.OIDC.WellKnown+import URI.ByteString.QQ++data Okta = Okta deriving (Eq, Show)++type instance IdpUserInfo Okta = OktaUser++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+ }++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 =+ [uri|https://foo.okta.com/oauth2/v1/token|]+ }++mkOktaIdp ::+ MonadIO m =>+ -- | Full domain with no http protocol. e.g. @foo.okta.com@+ Text ->+ ExceptT Text m (Idp Okta)+mkOktaIdp domain = do+ OpenIDConfigurationUris {..} <- fetchWellKnownUris domain+ pure+ ( defaultOktaIdp+ { idpUserInfoEndpoint = userinfoUri,+ idpAuthorizeEndpoint = authorizationUri,+ idpTokenEndpoint = tokenUri+ }+ )++-- | 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+ }+ deriving (Show, Generic)++instance FromJSON OktaUser where+ parseJSON =+ genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
+ src/Network/OAuth2/Provider/Slack.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.Slack where++import Data.Aeson+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 Slack = Slack deriving (Show, Eq)++type instance IdpUserInfo Slack = SlackUser++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+ }++-- https://api.slack.com/authentication/sign-in-with-slack+-- https://slack.com/.well-known/openid-configuration+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|]+ }++data SlackUser = SlackUser+ { name :: Text,+ email :: Text+ }+ deriving (Show, Generic)++instance FromJSON SlackUser
+ src/Network/OAuth2/Provider/StackExchange.hs view
@@ -0,0 +1,84 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++{-+ NOTES: stackexchange API spec and its document just sucks!+-}+module Network.OAuth2.Provider.StackExchange where++import Data.Aeson+import Data.ByteString (ByteString)+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+import URI.ByteString.QQ++-- fix key from your application edit page+-- https://stackapps.com/apps/oauth+stackexchangeAppKey :: ByteString+stackexchangeAppKey = ""++data StackExchange = StackExchange deriving (Eq, Show)++type instance IdpUserInfo StackExchange = StackExchangeResp++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+ }++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.+ -- 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|]+ }++data StackExchangeResp = StackExchangeResp+ { hasMore :: Bool,+ quotaMax :: Integer,+ quotaRemaining :: Integer,+ items :: [StackExchangeUser]+ }+ deriving (Show, Generic)++data StackExchangeUser = StackExchangeUser+ { userId :: Integer,+ displayName :: Text,+ profileImage :: Text+ }+ deriving (Show, Generic)++instance FromJSON StackExchangeResp where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++instance FromJSON StackExchangeUser where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++appendStackExchangeAppKey :: URI -> ByteString -> URI+appendStackExchangeAppKey useruri k = appendQueryParams [("key", k)] useruri
+ src/Network/OAuth2/Provider/Weibo.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE QuasiQuotes #-}++module Network.OAuth2.Provider.Weibo where++import Data.Aeson+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 Weibo = Weibo deriving (Eq, Show)++type instance IdpUserInfo Weibo = WeiboUID++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+ }++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|]+ }++-- | UserInfor API: http://open.weibo.com/wiki/2/users/show+data WeiboUser = WeiboUser+ { id :: Integer,+ name :: Text,+ screenName :: Text+ }+ deriving (Show, Generic)++newtype WeiboUID = WeiboUID {uid :: Integer}+ deriving (Show, Generic)++instance FromJSON WeiboUID ++instance FromJSON WeiboUser where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
+ src/Network/OAuth2/Provider/ZOHO.hs view
@@ -0,0 +1,60 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TypeFamilies #-}++module Network.OAuth2.Provider.ZOHO where++import Data.Aeson+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++-- `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++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+ }++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|]+ }++data ZOHOUser = ZOHOUser+ { email :: Text,+ fullName :: Text+ }+ deriving (Show, Generic)++newtype ZOHOUserResp = ZOHOUserResp {users :: [ZOHOUser]}+ deriving (Show, Generic)++instance FromJSON ZOHOUserResp ++instance FromJSON ZOHOUser where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}
+ src/Network/OIDC/WellKnown.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE DuplicateRecordFields #-}++module Network.OIDC.WellKnown where++import Control.Monad.Except+import Data.Aeson+import Data.Bifunctor+import Data.ByteString.Lazy (ByteString)+import Data.ByteString.Lazy qualified as BSL+import Data.Text.Lazy (Text)+import Data.Text.Lazy qualified as TL+import Data.Text.Lazy.Encoding qualified as TL+import GHC.Generics+import Network.HTTP.Simple+import Network.HTTP.Types.Status+import URI.ByteString++-- | Slim OpenID Configuration+-- TODO: could add more fields to be complete.+data OpenIDConfiguration = OpenIDConfiguration+ { issuer :: Text,+ authorizationEndpoint :: Text,+ tokenEndpoint :: Text,+ userinfoEndpoint :: Text,+ jwksUri :: Text+ }+ deriving (Generic)++data OpenIDConfigurationUris = OpenIDConfigurationUris+ { authorizationUri :: URI,+ tokenUri :: URI,+ userinfoUri :: URI,+ jwksUri :: URI+ }+ deriving (Generic)++instance FromJSON OpenIDConfiguration where+ parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++wellknownUrl :: TL.Text+wellknownUrl = "/.well-known/openid-configuration"++fetchWellKnown ::+ MonadIO m =>+ -- | Domain+ TL.Text ->+ ExceptT Text m OpenIDConfiguration+fetchWellKnown domain = ExceptT $ do+ let uri = "https://" <> domain <> wellknownUrl+ req <- liftIO $ parseRequest (TL.unpack uri)+ resp <- httpLbs req+ return (handleWellKnownResponse resp)++fetchWellKnownUris :: MonadIO m => TL.Text -> ExceptT Text m OpenIDConfigurationUris+fetchWellKnownUris domain = do+ OpenIDConfiguration {..} <- fetchWellKnown domain+ withExceptT (TL.pack . show) $ do+ ae <- ExceptT $ pure (parseURI strictURIParserOptions $ BSL.toStrict $ TL.encodeUtf8 authorizationEndpoint)+ te <- ExceptT $ pure (parseURI strictURIParserOptions $ BSL.toStrict $ TL.encodeUtf8 tokenEndpoint)+ ue <- ExceptT $ pure (parseURI strictURIParserOptions $ BSL.toStrict $ TL.encodeUtf8 userinfoEndpoint)+ jwks <- ExceptT $ pure (parseURI strictURIParserOptions $ BSL.toStrict $ TL.encodeUtf8 jwksUri)+ pure+ OpenIDConfigurationUris+ { authorizationUri = ae,+ tokenUri = te,+ userinfoUri = ue,+ jwksUri = jwks+ }++handleWellKnownResponse :: Response ByteString -> Either Text OpenIDConfiguration+handleWellKnownResponse resp = do+ let rawBody = getResponseBody resp+ let rStatus = getResponseStatus resp+ if rStatus == status200+ then first (("handleWellKnownResponse decode response failed: " <>) . TL.pack) (eitherDecode rawBody)+ else Left $ "handleWellKnownResponse failed: " <> TL.pack (show rStatus) <> TL.decodeUtf8 rawBody