hoauth2 1.16.2 → 2.0.0
raw patch · 11 files changed
+287/−166 lines, 11 filesdep ~mustachePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: mustache
API changes (from Hackage documentation)
- Network.OAuth.OAuth2.Internal: [oauthAccessTokenEndpoint] :: OAuth2 -> URI
- Network.OAuth.OAuth2.Internal: [oauthCallback] :: OAuth2 -> Maybe URI
- Network.OAuth.OAuth2.Internal: [oauthClientId] :: OAuth2 -> Text
- Network.OAuth.OAuth2.Internal: [oauthClientSecret] :: OAuth2 -> Maybe Text
- Network.OAuth.OAuth2.Internal: [oauthOAuthorizeEndpoint] :: OAuth2 -> URI
+ Network.OAuth.OAuth2.Internal: [oauth2AuthorizeEndpoint] :: OAuth2 -> URIRef Absolute
+ Network.OAuth.OAuth2.Internal: [oauth2ClientId] :: OAuth2 -> Text
+ Network.OAuth.OAuth2.Internal: [oauth2ClientSecret] :: OAuth2 -> Maybe Text
+ Network.OAuth.OAuth2.Internal: [oauth2RedirectUri] :: OAuth2 -> Maybe (URIRef Absolute)
+ Network.OAuth.OAuth2.Internal: [oauth2TokenEndpoint] :: OAuth2 -> URIRef Absolute
+ Network.OAuth.OAuth2.Internal: instance Data.Hashable.Class.Hashable Network.OAuth.OAuth2.Internal.OAuth2
- Network.OAuth.OAuth2.Internal: OAuth2 :: Text -> Maybe Text -> URI -> URI -> Maybe URI -> OAuth2
+ Network.OAuth.OAuth2.Internal: OAuth2 :: Text -> Maybe Text -> URIRef Absolute -> URIRef Absolute -> Maybe (URIRef Absolute) -> OAuth2
Files
- README.md +3/−1
- example/IDP.hs +2/−0
- example/IDP/Okta.hs +35/−26
- example/IDP/Slack.hs +63/−0
- example/Keys.hs +72/−60
- example/Keys.hs.sample +72/−60
- example/README.org +3/−0
- example/templates/index.mustache +2/−1
- hoauth2.cabal +5/−2
- src/Network/OAuth/OAuth2/HttpClient.hs +4/−4
- src/Network/OAuth/OAuth2/Internal.hs +26/−12
README.md view
@@ -1,4 +1,6 @@-[](http://travis-ci.org/freizl/hoauth2)+[](https://github.com/freizl/hoauth2/actions/workflows/build.yml)+[](https://github.com/freizl/hoauth2/actions/workflows/lint.yml)+[](http://travis-ci.org/freizl/hoauth2) [](https://hackage.haskell.org/package/hoauth2) # Introduction
example/IDP.hs view
@@ -13,6 +13,7 @@ import qualified IDP.Okta as IOkta import qualified IDP.StackExchange as IStackExchange import qualified IDP.Weibo as IWeibo+import qualified IDP.Slack as ISlack import qualified IDP.ZOHO as IZOHO import Session import Types@@ -32,6 +33,7 @@ IDPApp IStackExchange.StackExchange, IDPApp IWeibo.Weibo, IDPApp IAuth0.Auth0,+ IDPApp ISlack.Slack, IDPApp IZOHO.ZOHO ]
example/IDP/Okta.hs view
@@ -1,20 +1,21 @@-{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE QuasiQuotes #-} module IDP.Okta where-import Data.Aeson-import Data.Bifunctor-import Data.Hashable-import Data.Text.Lazy ( Text )-import GHC.Generics-import Keys-import Network.OAuth.OAuth2-import Types-import URI.ByteString-import URI.ByteString.QQ-import Utils +import Data.Aeson+import Data.Bifunctor+import Data.Hashable+import Data.Text.Lazy (Text)+import GHC.Generics+import Keys+import Network.OAuth.OAuth2+import Types+import URI.ByteString+import URI.ByteString.QQ+import Utils+ data Okta = Okta deriving (Show, Generic) @@ -35,28 +36,36 @@ re <- authGetJSON mgr at userInfoUri return (second toLoginUser re) +-- | 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)+-- instance HasAuthUri Okta where- authUri _ = createCodeUri- oktaKey- [ ("state", "Okta.test-state-123")- , ( "scope"- , "openid profile offline_access okta.users.read.self okta.users.read"- )- ]+ authUri _ =+ createCodeUri+ oktaKey+ [ ("state", "Okta.test-state-123"),+ ( "scope",+ "openid profile"+ -- , "openid profile offline_access okta.users.read.self okta.users.read"+ ),+ ("prompt", "login consent")+ ] data OktaUser = OktaUser- { name :: Text- , preferredUsername :: Text+ { name :: Text,+ preferredUsername :: Text } deriving (Show, Generic) instance FromJSON OktaUser where parseJSON =- genericParseJSON defaultOptions { fieldLabelModifier = camelTo2 '_' }+ genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'} userInfoUri :: URI-userInfoUri = [uri|http://rain.okta1.com:1802/oauth2/v1/userinfo|]+userInfoUri = [uri|https://hw2.trexcloud.com/oauth2/v1/userinfo|] -toLoginUser :: OktaUser -> LoginUser-toLoginUser ouser = LoginUser { loginUserName = name ouser }+-- userInfoUri = [uri|https://dev-148986.oktapreview.com/oauth2/v1/userinfo|] +toLoginUser :: OktaUser -> LoginUser+toLoginUser ouser = LoginUser {loginUserName = name ouser}
+ example/IDP/Slack.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++module IDP.Slack where++import Data.Aeson+import Data.Bifunctor+import Data.Hashable+import Data.Text.Lazy (Text)+import GHC.Generics+import Keys+import Network.OAuth.OAuth2+import Types+import URI.ByteString+import URI.ByteString.QQ+import Utils++data Slack = Slack+ deriving (Show, Generic)++instance Hashable Slack++instance IDP Slack++instance HasLabel Slack++instance HasTokenReq Slack where+ tokenReq _ mgr = fetchAccessToken mgr slackKey++instance HasTokenRefreshReq Slack where+ tokenRefreshReq _ mgr = refreshAccessToken mgr slackKey++instance HasUserReq Slack where+ userReq _ mgr at = do+ re <- authGetJSON mgr at userInfoUri+ return (second toLoginUser re)++instance HasAuthUri Slack where+ authUri _ =+ createCodeUri+ slackKey+ [ ("state", "Slack.test-state-123"),+ ( "scope",+ "openid profile email"+ )+ ]++data SlackUser = SlackUser+ { name :: Text,+ email :: Text+ }+ deriving (Show, Generic)++instance FromJSON SlackUser where+ parseJSON =+ genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}++userInfoUri :: URI+userInfoUri = [uri|https://slack.com/api/openid.connect.userInfo|]++toLoginUser :: SlackUser -> LoginUser+toLoginUser ouser = LoginUser {loginUserName = name ouser}
example/Keys.hs view
@@ -9,60 +9,60 @@ weiboKey :: OAuth2 weiboKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/oauthCallback|]- , oauthOAuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/oauthCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|] } -- | http://developer.github.com/v3/oauth/ githubKey :: OAuth2 githubKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/githubCallback|]- , oauthOAuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/githubCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|]+ , oauth2TokenEndpoint = [uri|https://github.com/login/oauth/access_token|] } -- | oauthCallback = Just "https://developers.google.com/oauthplayground" googleKey :: OAuth2 googleKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx.apps.googleusercontent.com"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/googleCallback|]- , oauthOAuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/auth|]- , oauthAccessTokenEndpoint = [uri|https://www.googleapis.com/oauth2/v3/token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx.apps.googleusercontent.com"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/googleCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/auth|]+ , oauth2TokenEndpoint = [uri|https://www.googleapis.com/oauth2/v3/token|] } facebookKey :: OAuth2 facebookKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://t.haskellcn.org/cb|]- , oauthOAuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://t.haskellcn.org/cb|]+ , oauth2AuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|]+ , oauth2TokenEndpoint = [uri|https://graph.facebook.com/v2.3/oauth/access_token|] } doubanKey :: OAuth2 doubanKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://localhost:9999/oauthCallback|]- , oauthOAuthorizeEndpoint = [uri|https://www.douban.com/service/auth2/auth|]- , oauthAccessTokenEndpoint = [uri|https://www.douban.com/service/auth2/token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9999/oauthCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.douban.com/service/auth2/auth|]+ , oauth2TokenEndpoint = [uri|https://www.douban.com/service/auth2/token|] } fitbitKey :: OAuth2 fitbitKey = OAuth2- { oauthClientId = "xxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|]+ { oauth2ClientId = "xxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|] } -- fix key from your application edit page@@ -72,58 +72,70 @@ stackexchangeKey :: OAuth2 stackexchangeKey = OAuth2- { oauthClientId = "xx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://c.haskellcn.org/cb|]- , oauthOAuthorizeEndpoint = [uri|https://stackexchange.com/oauth|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://c.haskellcn.org/cb|]+ , oauth2AuthorizeEndpoint = [uri|https://stackexchange.com/oauth|]+ , oauth2TokenEndpoint = [uri|https://stackexchange.com/oauth/access_token|] } dropboxKey :: OAuth2 dropboxKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|] } oktaKey :: OAuth2 oktaKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint =+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://dev-148986.oktapreview.com/oauth2/v1/authorize|]- , oauthAccessTokenEndpoint =+ , oauth2TokenEndpoint = [uri|https://dev-148986.oktapreview.com/oauth2/v1/token|] } azureADKey :: OAuth2 azureADKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint =+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://login.windows.net/common/oauth2/authorize|]- , oauthAccessTokenEndpoint =+ , oauth2TokenEndpoint = [uri|https://login.windows.net/common/oauth2/token|] } zohoKey :: OAuth2 zohoKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|]- , oauthAccessTokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|]+ , oauth2TokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|] } auth0Key :: OAuth2 auth0Key = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|]- , oauthAccessTokenEndpoint = [uri|https://foo.auth0.com/oauth/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|]+ , oauth2TokenEndpoint = [uri|https://foo.auth0.com/oauth/token|] }++-- https://api.slack.com/authentication/sign-in-with-slack+-- https://slack.com/.well-known/openid-configuration+slackKey :: OAuth2+slackKey =+ OAuth2+ { oauth2ClientId = ""+ , oauth2ClientSecret = Just ""+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://slack.com/openid/connect/authorize|]+ , oauth2TokenEndpoint = [uri|https://slack.com/api/openid.connect.token|]+ }
example/Keys.hs.sample view
@@ -9,60 +9,60 @@ weiboKey :: OAuth2 weiboKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/oauthCallback|]- , oauthOAuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/oauthCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://api.weibo.com/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.weibo.com/oauth2/access_token|] } -- | http://developer.github.com/v3/oauth/ githubKey :: OAuth2 githubKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/githubCallback|]- , oauthOAuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/githubCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://github.com/login/oauth/authorize|]+ , oauth2TokenEndpoint = [uri|https://github.com/login/oauth/access_token|] } -- | oauthCallback = Just "https://developers.google.com/oauthplayground" googleKey :: OAuth2 googleKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx.apps.googleusercontent.com"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://127.0.0.1:9988/googleCallback|]- , oauthOAuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/auth|]- , oauthAccessTokenEndpoint = [uri|https://www.googleapis.com/oauth2/v3/token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx.apps.googleusercontent.com"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://127.0.0.1:9988/googleCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://accounts.google.com/o/oauth2/auth|]+ , oauth2TokenEndpoint = [uri|https://www.googleapis.com/oauth2/v3/token|] } facebookKey :: OAuth2 facebookKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://t.haskellcn.org/cb|]- , oauthOAuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://t.haskellcn.org/cb|]+ , oauth2AuthorizeEndpoint = [uri|https://www.facebook.com/dialog/oauth|]+ , oauth2TokenEndpoint = [uri|https://graph.facebook.com/v2.3/oauth/access_token|] } doubanKey :: OAuth2 doubanKey = OAuth2- { oauthClientId = "xxxxxxxxxxxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://localhost:9999/oauthCallback|]- , oauthOAuthorizeEndpoint = [uri|https://www.douban.com/service/auth2/auth|]- , oauthAccessTokenEndpoint = [uri|https://www.douban.com/service/auth2/token|]+ { oauth2ClientId = "xxxxxxxxxxxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9999/oauthCallback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.douban.com/service/auth2/auth|]+ , oauth2TokenEndpoint = [uri|https://www.douban.com/service/auth2/token|] } fitbitKey :: OAuth2 fitbitKey = OAuth2- { oauthClientId = "xxxxxx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|]+ { oauth2ClientId = "xxxxxx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.fitbit.com/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.fitbit.com/oauth2/token|] } -- fix key from your application edit page@@ -72,58 +72,70 @@ stackexchangeKey :: OAuth2 stackexchangeKey = OAuth2- { oauthClientId = "xx"- , oauthClientSecret = Just "xxxxxxxxxxxxxxx"- , oauthCallback = Just [uri|http://c.haskellcn.org/cb|]- , oauthOAuthorizeEndpoint = [uri|https://stackexchange.com/oauth|]- , oauthAccessTokenEndpoint =+ { oauth2ClientId = "xx"+ , oauth2ClientSecret = Just "xxxxxxxxxxxxxxx"+ , oauth2RedirectUri = Just [uri|http://c.haskellcn.org/cb|]+ , oauth2AuthorizeEndpoint = [uri|https://stackexchange.com/oauth|]+ , oauth2TokenEndpoint = [uri|https://stackexchange.com/oauth/access_token|] } dropboxKey :: OAuth2 dropboxKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|]- , oauthAccessTokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://www.dropbox.com/1/oauth2/authorize|]+ , oauth2TokenEndpoint = [uri|https://api.dropboxapi.com/oauth2/token|] } oktaKey :: OAuth2 oktaKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint =+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://dev-148986.oktapreview.com/oauth2/v1/authorize|]- , oauthAccessTokenEndpoint =+ , oauth2TokenEndpoint = [uri|https://dev-148986.oktapreview.com/oauth2/v1/token|] } azureADKey :: OAuth2 azureADKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint =+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://login.windows.net/common/oauth2/authorize|]- , oauthAccessTokenEndpoint =+ , oauth2TokenEndpoint = [uri|https://login.windows.net/common/oauth2/token|] } zohoKey :: OAuth2 zohoKey = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|]- , oauthAccessTokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://accounts.zoho.com/oauth/v2/auth|]+ , oauth2TokenEndpoint = [uri|https://accounts.zoho.com/oauth/v2/token|] } auth0Key :: OAuth2 auth0Key = OAuth2- { oauthClientId = "xxx"- , oauthClientSecret = Just "xxx"- , oauthCallback = Just [uri|http://localhost:9988/oauth2/callback|]- , oauthOAuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|]- , oauthAccessTokenEndpoint = [uri|https://foo.auth0.com/oauth/token|]+ { oauth2ClientId = "xxx"+ , oauth2ClientSecret = Just "xxx"+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://foo.auth0.com/authorize|]+ , oauth2TokenEndpoint = [uri|https://foo.auth0.com/oauth/token|] }++-- https://api.slack.com/authentication/sign-in-with-slack+-- https://slack.com/.well-known/openid-configuration+slackKey :: OAuth2+slackKey =+ OAuth2+ { oauth2ClientId = ""+ , oauth2ClientSecret = Just ""+ , oauth2RedirectUri = Just [uri|http://localhost:9988/oauth2/callback|]+ , oauth2AuthorizeEndpoint = [uri|https://slack.com/openid/connect/authorize|]+ , oauth2TokenEndpoint = [uri|https://slack.com/api/openid.connect.token|]+ }
example/README.org view
@@ -18,6 +18,9 @@ - <https://developer.linkedin.com> +* TODO+- [ ] Split ~Key.hs~ to each IDP and read ~.env~ file if exists for override+ * NOTES - classes in Types.hs takes a (`IDP`) as first parameter but it is actually not used. bad pattern. how to fix it?? - refactor: `App.hs` is messy!
example/templates/index.mustache view
@@ -8,7 +8,7 @@ <section class="login-with"> {{^isLogin}}- <a href="{{codeFlowUri}}">Login {{name}}</a>+ <a href="{{codeFlowUri}}">Sign in with {{name}}</a> {{/isLogin}} {{#isLogin}}@@ -25,6 +25,7 @@ <h2>Notes</h2> <ol> <li>for StackExchange, the callback domain is localhost, have manually add port 9988.</li>+ <li>for Slack, the callback url has to be https and without any port hence need manual intervention. (TODO: add tls to the server)</li> </ol> </p> </body>
hoauth2.cabal view
@@ -1,7 +1,7 @@ Cabal-version: 2.4 Name: hoauth2 -- http://wiki.haskell.org/Package_versioning_policy-Version: 1.16.2+Version: 2.0.0 Synopsis: Haskell OAuth2 authentication client @@ -46,6 +46,7 @@ example/IDP/Fitbit.hs example/IDP/Douban.hs example/IDP/Linkedin.hs+ example/IDP/Slack.hs example/IDP/Auth0.hs example/IDP.hs example/App.hs@@ -86,6 +87,7 @@ uri-bytestring >= 0.2.3.1 && < 0.4, uri-bytestring-aeson >= 0.1 && < 0.2, microlens >= 0.4.0 && < 0.5,+ hashable >= 1.2.6 && < 1.5.0, exceptions >= 0.8.3 && < 0.11 ghc-options: -Wall -fwarn-tabs -funbox-strict-fields@@ -107,6 +109,7 @@ IDP.Fitbit IDP.Github IDP.Google+ IDP.Slack IDP.Auth0 IDP.Okta IDP.StackExchange@@ -132,7 +135,7 @@ microlens >= 0.4.0 && < 0.5, unordered-containers >= 0.2.5, wai-middleware-static >= 0.8.1 && < 0.10.0,- mustache >= 2.2.3 && < 2.5.0,+ mustache >= 2.2.3 && < 2.4.0, scotty >= 0.10.0 && < 0.13, binary >= 0.8.3.0 && < 0.8.9, parsec >= 3.1.11 && < 3.2.0 ,
src/Network/OAuth/OAuth2/HttpClient.hs view
@@ -65,7 +65,7 @@ fetchAccessToken2 mgr oa code = do let (url, body1) = accessTokenUrl oa code let secret x = [("client_secret", T.encodeUtf8 x)]- let extraBody = ("client_id", T.encodeUtf8 $ oauthClientId oa) : maybe [] secret (oauthClientSecret oa)+ let extraBody = ("client_id", T.encodeUtf8 $ oauth2ClientId oa) : maybe [] secret (oauth2ClientSecret oa) doJSONPostRequest mgr oa url (extraBody ++ body1) -- | Fetch a new AccessToken with the Refresh Token with authentication in request header.@@ -92,7 +92,7 @@ refreshAccessToken2 manager oa token = do let (uri, body) = refreshAccessTokenUrl oa token let secret x = [("client_secret", T.encodeUtf8 x)]- let extraBody = ("client_id", T.encodeUtf8 $ oauthClientId oa) : maybe [] secret (oauthClientSecret oa)+ let extraBody = ("client_id", T.encodeUtf8 $ oauth2ClientId oa) : maybe [] secret (oauth2ClientSecret oa) doJSONPostRequest manager oa uri (extraBody ++ body) -- | Conduct post request and return response as JSON.@@ -113,8 +113,8 @@ doSimplePostRequest manager oa url body = fmap handleOAuth2TokenResponse go where go = do req <- uriToRequest url- let addBasicAuth = case oauthClientSecret oa of- (Just secret) -> applyBasicAuth (T.encodeUtf8 $ oauthClientId oa) (T.encodeUtf8 secret)+ let addBasicAuth = case oauth2ClientSecret oa of+ (Just secret) -> applyBasicAuth (T.encodeUtf8 $ oauth2ClientId oa) (T.encodeUtf8 secret) Nothing -> id req' = (addBasicAuth . updateRequestHeaders Nothing) req httpLbs (urlEncodedBody body req') manager
src/Network/OAuth/OAuth2/Internal.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE RecordWildCards #-} {-# OPTIONS_HADDOCK -ignore-exports #-} -- | A simple OAuth2 Haskell binding. (This is supposed to be@@ -19,13 +20,14 @@ import Data.Maybe import Data.Text (Text, pack, unpack) import Data.Text.Encoding-import GHC.Generics import Lens.Micro import Lens.Micro.Extras import Network.HTTP.Conduit as C import qualified Network.HTTP.Types as H import URI.ByteString import URI.ByteString.Aeson ()+import Data.Hashable+import GHC.Generics -------------------------------------------------- @@ -34,15 +36,27 @@ -------------------------------------------------- -- | Query Parameter Representation+-- TODO: fix typo in OAuthorizeEndpoint+-- rename AccessToken to TokenEndpoint+-- rename callback to redirectUri+ data OAuth2 = OAuth2- { oauthClientId :: Text,- oauthClientSecret :: Maybe Text,- oauthOAuthorizeEndpoint :: URI,- oauthAccessTokenEndpoint :: URI,- oauthCallback :: Maybe URI+ { oauth2ClientId :: Text,+ oauth2ClientSecret :: Maybe Text,+ oauth2AuthorizeEndpoint :: URIRef Absolute,+ oauth2TokenEndpoint :: URIRef Absolute,+ oauth2RedirectUri :: Maybe ( URIRef Absolute ) } deriving (Show, Eq) +instance Hashable OAuth2 where+ hashWithSalt salt OAuth2{..} = salt+ `hashWithSalt` hash oauth2ClientId+ `hashWithSalt` hash oauth2ClientSecret+ `hashWithSalt` hash (show oauth2AuthorizeEndpoint)+ `hashWithSalt` hash (show oauth2TokenEndpoint)+ `hashWithSalt` hash (show oauth2RedirectUri)+ newtype AccessToken = AccessToken {atoken :: Text} deriving (Binary, Eq, Show, FromJSON, ToJSON) newtype RefreshToken = RefreshToken {rtoken :: Text} deriving (Binary, Eq, Show, FromJSON, ToJSON)@@ -138,13 +152,13 @@ -- | Prepare the authorization URL. Redirect to this URL -- asking for user interactive authentication. authorizationUrl :: OAuth2 -> URI-authorizationUrl oa = over (queryL . queryPairsL) (++ queryParts) (oauthOAuthorizeEndpoint oa)+authorizationUrl oa = over (queryL . queryPairsL) (++ queryParts) (oauth2AuthorizeEndpoint oa) where queryParts = catMaybes- [ Just ("client_id", encodeUtf8 $ oauthClientId oa),+ [ Just ("client_id", encodeUtf8 $ oauth2ClientId oa), Just ("response_type", "code"),- fmap (("redirect_uri",) . serializeURIRef') (oauthCallback oa)+ fmap (("redirect_uri",) . serializeURIRef') (oauth2RedirectUri oa) ] -- | Prepare the URL and the request body query for fetching an access token.@@ -168,11 +182,11 @@ (URI, PostBody) accessTokenUrl' oa code gt = (uri, body) where- uri = oauthAccessTokenEndpoint oa+ uri = oauth2TokenEndpoint oa body = catMaybes [ Just ("code", encodeUtf8 $ extoken code),- ("redirect_uri",) . serializeURIRef' <$> oauthCallback oa,+ ("redirect_uri",) . serializeURIRef' <$> oauth2RedirectUri oa, fmap (("grant_type",) . encodeUtf8) gt ] @@ -186,7 +200,7 @@ (URI, PostBody) refreshAccessTokenUrl oa token = (uri, body) where- uri = oauthAccessTokenEndpoint oa+ uri = oauth2TokenEndpoint oa body = [ ("grant_type", "refresh_token"), ("refresh_token", encodeUtf8 $ rtoken token)