Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
This module contains a new way of doing OAuth2 authorization and authentication in order to obtain Access Token and maybe Refresh Token base on rfc6749.
This module will become default in future release.
The key concept/change is to introduce the Grant flow, which determines the entire work flow per spec. Each work flow will have slight different request parameters, which often time you'll see different configuration when creating OAuth2 application in the IdP developer application page.
Here are supported flows
- Authorization Code. This flow requires authorize call to obtain an authorize code, then exchange the code for tokens.
- Resource Owner Password. This flow only requires to hit token endpoint with, of course, username and password, to obtain tokens.
- Client Credentials. This flow also only requires to hit token endpoint but with different parameters. Client credentials flow does not involve an end user hence you won't be able to hit userinfo endpoint with access token obtained.
- PKCE (rfc7636). This is enhancement on top of authorization code flow.
Implicit flow is not supported because it is more for SPA (single page app) given it is deprecated by Authorization Code flow with PKCE.
Here is quick sample for how to use vocabularies from this new module.
Firstly, initialize your IdP (use google as example) and the application.
import Network.OAuth2.Experiment import URI.ByteString.QQ data Google = Google deriving (Eq, Show) googleIdp :: Idp Google googleIdp = Idp { 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|] , idpDeviceAuthorizationEndpoint = Just [uri|https://oauth2.googleapis.com/device/code|] } fooApp :: AuthorizationCodeApplication fooApp = AuthorizationCodeApplication { acClientId = "xxxxx", acClientSecret = "xxxxx", acScope = Set.fromList [ "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile" ], acAuthorizeState = "CHANGE_ME", acAuthorizeRequestExtraParams = Map.empty, acRedirectUri = [uri|http://localhost/oauth2/callback|], acName = "sample-google-authorization-code-app", acTokenRequestAuthenticationMethod = ClientSecretBasic, } fooIdpApplication :: IdpApplication AuthorizationCodeApplication Google fooIdpApplication = IdpApplication fooApp googleIdp
Secondly, construct the authorize URL.
authorizeUrl = mkAuthorizationRequest fooIdpApplication
Thirdly, after a successful redirect with authorize code, you could exchange for access token
mgr <- liftIO $ newManager tlsManagerSettings tokenResp <- conduitTokenRequest fooIdpApplication mgr authorizeCode
If you'd like to fetch user info, uses this method
conduitUserInfoRequest fooIdpApplication mgr (accessToken tokenResp)
You could also find example from hoauth2-providers-tutorials
module.
Synopsis
- data AuthorizationCodeApplication = AuthorizationCodeApplication {}
- data DeviceAuthorizationApplication = DeviceAuthorizationApplication {}
- pollDeviceTokenRequest :: MonadIO m => IdpApplication i DeviceAuthorizationApplication -> Manager -> DeviceAuthorizationResponse -> ExceptT TokenResponseError m OAuth2Token
- data ClientCredentialsApplication = ClientCredentialsApplication {}
- data ResourceOwnerPasswordApplication = ResourceOwnerPasswordApplication {}
- data JwtBearerApplication = JwtBearerApplication {}
- class HasAuthorizeRequest a
- mkAuthorizationRequest :: HasAuthorizeRequest a => IdpApplication i a -> URI
- mkPkceAuthorizeRequest :: (MonadIO m, HasPkceAuthorizeRequest a) => IdpApplication i a -> m (URI, CodeVerifier)
- class HasOAuth2Key a => HasDeviceAuthorizationRequest a
- data DeviceAuthorizationResponse = DeviceAuthorizationResponse {}
- conduitDeviceAuthorizationRequest :: (MonadIO m, HasDeviceAuthorizationRequest a) => IdpApplication i a -> Manager -> ExceptT ByteString m DeviceAuthorizationResponse
- class (HasOAuth2Key a, HasTokenRequestClientAuthenticationMethod a) => HasTokenRequest a where
- data TokenRequest a
- type ExchangeTokenInfo a
- data NoNeedExchangeToken = NoNeedExchangeToken
- conduitTokenRequest :: (HasTokenRequest a, ToQueryParam (TokenRequest a), MonadIO m) => IdpApplication i a -> Manager -> ExchangeTokenInfo a -> ExceptT TokenResponseError m OAuth2Token
- conduitPkceTokenRequest :: (HasTokenRequest a, ToQueryParam (TokenRequest a), MonadIO m) => IdpApplication i a -> Manager -> (ExchangeTokenInfo a, CodeVerifier) -> ExceptT TokenResponseError m OAuth2Token
- class (HasOAuth2Key a, HasTokenRequestClientAuthenticationMethod a) => HasRefreshTokenRequest a
- conduitRefreshTokenRequest :: (MonadIO m, HasRefreshTokenRequest a) => IdpApplication i a -> Manager -> RefreshToken -> ExceptT TokenResponseError m OAuth2Token
- class HasUserInfoRequest a
- conduitUserInfoRequest :: (MonadIO m, HasUserInfoRequest a, FromJSON b) => IdpApplication i a -> Manager -> AccessToken -> ExceptT ByteString m b
- conduitUserInfoRequestWithCustomMethod :: (MonadIO m, HasUserInfoRequest a, FromJSON b) => (Manager -> AccessToken -> URI -> ExceptT ByteString m b) -> IdpApplication i a -> Manager -> AccessToken -> ExceptT ByteString m b
- class HasOAuth2Key a
- newtype Password = Password {
- unPassword :: Text
- newtype Username = Username {
- unUsername :: Text
- newtype AuthorizeState = AuthorizeState {}
- newtype RedirectUri = RedirectUri {
- unRedirectUri :: URI
- newtype ClientSecret = ClientSecret {}
- newtype ClientId = ClientId {
- unClientId :: Text
- newtype Scope = Scope {}
- data IdpApplication (i :: k) a = IdpApplication {
- idp :: Idp i
- application :: a
- data Idp (i :: k) = Idp {}
- newtype CodeVerifier = CodeVerifier {}
- data ClientAuthenticationMethod
- uriToText :: URI -> Text
Application per Grant type
data AuthorizationCodeApplication Source #
An Application that supports "Authorization code" flow
Instances
data DeviceAuthorizationApplication Source #
An Application that supports "Device Authorization Grant"
DeviceAuthorizationApplication | |
|
Instances
pollDeviceTokenRequest :: MonadIO m => IdpApplication i DeviceAuthorizationApplication -> Manager -> DeviceAuthorizationResponse -> ExceptT TokenResponseError m OAuth2Token Source #
data ClientCredentialsApplication Source #
An Application that supports "Client Credentials" flow
Instances
data ResourceOwnerPasswordApplication Source #
An Application that supports "Resource Owner Password" flow
Instances
data JwtBearerApplication Source #
An Application that supports "JWT Bearer" flow
Instances
Authorization Code
class HasAuthorizeRequest a Source #
mkAuthorizationRequest :: HasAuthorizeRequest a => IdpApplication i a -> URI Source #
Constructs Authorization Code request URI https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
mkPkceAuthorizeRequest :: (MonadIO m, HasPkceAuthorizeRequest a) => IdpApplication i a -> m (URI, CodeVerifier) Source #
Constructs Authorization Code (PKCE) request URI and the Code Verifier. https://datatracker.ietf.org/doc/html/rfc7636
Device Authorization
class HasOAuth2Key a => HasDeviceAuthorizationRequest a Source #
conduitDeviceAuthorizationRequest :: (MonadIO m, HasDeviceAuthorizationRequest a) => IdpApplication i a -> Manager -> ExceptT ByteString m DeviceAuthorizationResponse Source #
Makes Device Authorization Request https://www.rfc-editor.org/rfc/rfc8628#section-3.1
Token Request
class (HasOAuth2Key a, HasTokenRequestClientAuthenticationMethod a) => HasTokenRequest a Source #
data TokenRequest a Source #
type ExchangeTokenInfo a Source #
Instances
data NoNeedExchangeToken Source #
Only Authorization Code Grant involves a Exchange Token (Authorization Code). ResourceOwnerPassword and Client Credentials make token request directly.
conduitTokenRequest :: (HasTokenRequest a, ToQueryParam (TokenRequest a), MonadIO m) => IdpApplication i a -> Manager -> ExchangeTokenInfo a -> ExceptT TokenResponseError m OAuth2Token Source #
Make Token Request https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3
conduitPkceTokenRequest :: (HasTokenRequest a, ToQueryParam (TokenRequest a), MonadIO m) => IdpApplication i a -> Manager -> (ExchangeTokenInfo a, CodeVerifier) -> ExceptT TokenResponseError m OAuth2Token Source #
Make Token Request (PKCE) https://datatracker.ietf.org/doc/html/rfc7636#section-4.5
Refresh Token Request
class (HasOAuth2Key a, HasTokenRequestClientAuthenticationMethod a) => HasRefreshTokenRequest a Source #
conduitRefreshTokenRequest :: (MonadIO m, HasRefreshTokenRequest a) => IdpApplication i a -> Manager -> RefreshToken -> ExceptT TokenResponseError m OAuth2Token Source #
Make Refresh Token Request https://www.rfc-editor.org/rfc/rfc6749#section-6
UserInfo Request
class HasUserInfoRequest a Source #
conduitUserInfoRequest :: (MonadIO m, HasUserInfoRequest a, FromJSON b) => IdpApplication i a -> Manager -> AccessToken -> ExceptT ByteString m b Source #
Standard approach of fetching /userinfo
conduitUserInfoRequestWithCustomMethod :: (MonadIO m, HasUserInfoRequest a, FromJSON b) => (Manager -> AccessToken -> URI -> ExceptT ByteString m b) -> IdpApplication i a -> Manager -> AccessToken -> ExceptT ByteString m b Source #
Usually conduitUserInfoRequest
is good enough.
But some IdP has different approach to fetch user information rather than GET.
This method gives the flexiblity.
Types
class HasOAuth2Key a Source #
Instances
Instances
IsString Password Source # | |
Defined in Network.OAuth2.Experiment.Types fromString :: String -> Password # | |
Eq Password Source # | |
ToQueryParam Password Source # | |
Defined in Network.OAuth2.Experiment.Types |
Instances
IsString Username Source # | |
Defined in Network.OAuth2.Experiment.Types fromString :: String -> Username # | |
Eq Username Source # | |
ToQueryParam Username Source # | |
Defined in Network.OAuth2.Experiment.Types |
newtype AuthorizeState Source #
Instances
IsString AuthorizeState Source # | |
Defined in Network.OAuth2.Experiment.Types fromString :: String -> AuthorizeState # | |
Eq AuthorizeState Source # | |
Defined in Network.OAuth2.Experiment.Types (==) :: AuthorizeState -> AuthorizeState -> Bool # (/=) :: AuthorizeState -> AuthorizeState -> Bool # | |
ToQueryParam AuthorizeState Source # | |
Defined in Network.OAuth2.Experiment.Types toQueryParam :: AuthorizeState -> Map Text Text Source # |
newtype RedirectUri Source #
Instances
Eq RedirectUri Source # | |
Defined in Network.OAuth2.Experiment.Types (==) :: RedirectUri -> RedirectUri -> Bool # (/=) :: RedirectUri -> RedirectUri -> Bool # | |
ToQueryParam RedirectUri Source # | |
Defined in Network.OAuth2.Experiment.Types toQueryParam :: RedirectUri -> Map Text Text Source # |
newtype ClientSecret Source #
Can be either "Client Secret" or JWT base on client authentication method
Instances
IsString ClientSecret Source # | |
Defined in Network.OAuth2.Experiment.Types fromString :: String -> ClientSecret # | |
Eq ClientSecret Source # | |
Defined in Network.OAuth2.Experiment.Types (==) :: ClientSecret -> ClientSecret -> Bool # (/=) :: ClientSecret -> ClientSecret -> Bool # | |
ToQueryParam ClientSecret Source # | |
Defined in Network.OAuth2.Experiment.Types toQueryParam :: ClientSecret -> Map Text Text Source # |
data IdpApplication (i :: k) a Source #
An OAuth2 Application "a" of IdP "i". "a" can be one of following type:
IdpApplication | |
|
Idp i
consists various endpoints endpoints.
The i
is actually phantom type for information only (Idp name) at this moment.
And it is PolyKinds.
Hence whenever Idp i
or IdpApplication i a
is used as function parameter,
PolyKinds need to be enabled.
Idp | |
|
newtype CodeVerifier Source #
Instances
ToQueryParam CodeVerifier Source # | |
Defined in Network.OAuth2.Experiment.Types toQueryParam :: CodeVerifier -> Map Text Text Source # |
data ClientAuthenticationMethod Source #
https://www.rfc-editor.org/rfc/rfc6749#section-2.3 According to spec:
The client MUST NOT use more than one authentication method in each request.
Which means use Authorization header or Post body.
However, I found I have to include authentication in the header all the time in real world.
In other words, ClientSecretBasic
is always assured. ClientSecretPost
is optional.
Maybe consider an alternative implementation that boolean kind of data type is good enough.