packages feed

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 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