diff --git a/hoauth2-providers.cabal b/hoauth2-providers.cabal
--- a/hoauth2-providers.cabal
+++ b/hoauth2-providers.cabal
@@ -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
diff --git a/src/Network/OAuth2/Provider/Auth0.hs b/src/Network/OAuth2/Provider/Auth0.hs
--- a/src/Network/OAuth2/Provider/Auth0.hs
+++ b/src/Network/OAuth2/Provider/Auth0.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/AzureAD.hs b/src/Network/OAuth2/Provider/AzureAD.hs
--- a/src/Network/OAuth2/Provider/AzureAD.hs
+++ b/src/Network/OAuth2/Provider/AzureAD.hs
@@ -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 '_'}
diff --git a/src/Network/OAuth2/Provider/Dropbox.hs b/src/Network/OAuth2/Provider/Dropbox.hs
--- a/src/Network/OAuth2/Provider/Dropbox.hs
+++ b/src/Network/OAuth2/Provider/Dropbox.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Facebook.hs b/src/Network/OAuth2/Provider/Facebook.hs
--- a/src/Network/OAuth2/Provider/Facebook.hs
+++ b/src/Network/OAuth2/Provider/Facebook.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Fitbit.hs b/src/Network/OAuth2/Provider/Fitbit.hs
--- a/src/Network/OAuth2/Provider/Fitbit.hs
+++ b/src/Network/OAuth2/Provider/Fitbit.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/Github.hs b/src/Network/OAuth2/Provider/Github.hs
--- a/src/Network/OAuth2/Provider/Github.hs
+++ b/src/Network/OAuth2/Provider/Github.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Google.hs b/src/Network/OAuth2/Provider/Google.hs
--- a/src/Network/OAuth2/Provider/Google.hs
+++ b/src/Network/OAuth2/Provider/Google.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/Linkedin.hs b/src/Network/OAuth2/Provider/Linkedin.hs
--- a/src/Network/OAuth2/Provider/Linkedin.hs
+++ b/src/Network/OAuth2/Provider/Linkedin.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Okta.hs b/src/Network/OAuth2/Provider/Okta.hs
--- a/src/Network/OAuth2/Provider/Okta.hs
+++ b/src/Network/OAuth2/Provider/Okta.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/Slack.hs b/src/Network/OAuth2/Provider/Slack.hs
--- a/src/Network/OAuth2/Provider/Slack.hs
+++ b/src/Network/OAuth2/Provider/Slack.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/StackExchange.hs b/src/Network/OAuth2/Provider/StackExchange.hs
--- a/src/Network/OAuth2/Provider/StackExchange.hs
+++ b/src/Network/OAuth2/Provider/StackExchange.hs
@@ -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)
 
diff --git a/src/Network/OAuth2/Provider/Twitter.hs b/src/Network/OAuth2/Provider/Twitter.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/OAuth2/Provider/Twitter.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Utils.hs b/src/Network/OAuth2/Provider/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/OAuth2/Provider/Utils.hs
@@ -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
diff --git a/src/Network/OAuth2/Provider/Weibo.hs b/src/Network/OAuth2/Provider/Weibo.hs
--- a/src/Network/OAuth2/Provider/Weibo.hs
+++ b/src/Network/OAuth2/Provider/Weibo.hs
@@ -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 '_'}
diff --git a/src/Network/OAuth2/Provider/ZOHO.hs b/src/Network/OAuth2/Provider/ZOHO.hs
--- a/src/Network/OAuth2/Provider/ZOHO.hs
+++ b/src/Network/OAuth2/Provider/ZOHO.hs
@@ -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 '_'}
diff --git a/src/Network/OIDC/WellKnown.hs b/src/Network/OIDC/WellKnown.hs
--- a/src/Network/OIDC/WellKnown.hs
+++ b/src/Network/OIDC/WellKnown.hs
@@ -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
