Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
If you're hurry, go check source code directly.
Configure your OAuth2 provider
Pick which OAuth2 provider you'd to use, e.g. Google, Github, Auth0 etc.
Pretty much all standard OAuth2 provider has developer portal to guide developer to use oauth2 flow.
So read it through if you're unfamiliar OAuth2 before.
Often time, those documents will guide you how to create an Application which has credentials
(e.g. client_id
and client_secret
for a web application), which will be used to authenticate your
service (replying party) with server.
For some OIDC providers, you may even be able to find out those URLs from a well-known endpoint.
https://BASE_DOMAIN/.well-known/openid-configuration
In this tutorial, I choose Auth0, which is one of existing OAuth2/OIDC Providers in the market. This is the API Docs https://auth0.com/docs/api
Generate Authorization URL.
OAuth2 starts with authorization.
To generate an authorization URL, call method authorizationUrl
, then call appendQueryParams
to
append additional query parameters, e.g. state
, scope
etc.
That method will also automatically append following query parameter to the authorization url.
client_id =xxx
-- client id of your Application credential you got previously response_type =code
-- must be for authorization request redirect_uri =xxx
-- where does the server (provider) send back the authorization code. -- You have to config this when creating Application at previous step.
The generated URL looks like
https://DOMAIN/path/to/authorize?client_id=xxx&response_type=code&redirect_uri=xxx&state=xxx&scope=xxx&..
Notes: As of today, hoauth2
only supports Code Grant
.
Redirect user to the Authorization URL
Now you need to have your user to navigate to that URL to kick off OAuth flow.
There are different ways to redirect user to the authorizeUrl
.
e.g.
- Display as anchor link directly at UI so that user can click it.
- Create your own login endpoint, e.g.
/login
, which then 302 to theauthorizeUrl
.
In this tutorial, I choose the second option. For instance this is how indexH
is implemented.
>>>
setHeader "Location" (uriToText authorizeUrl)
>>>
status status302
Obtain Access Token
When user navigates to authorizeUrl
, user will be prompt for login against the OAuth provider.
After an successful login there, user will be redirect back to your Application's redirect_uri
with code
in the query parameter.
With this code
, we could exchange for an Access Token.
Also you'd better to validate the state
is exactly what you pass in the authorizeUrl
.
OAuth2 provider expects to send the exact state
back in the redirect request.
To obtain an Access Token, you could call fetchAccessToken
,
which essentially takes the authorization code
, make request to OAuth2 provider's /token
endpoint
to get an Access Token, plus some other information (see details at OAuth2Token
).
fetchAccessToken
returns ExceptT (OAuth2Error Errors) m OAuth2Token
However Scotty, which is web framework I used to build this tutorial,
requires error as Text hence the transform with oauth2ErrorToText
Once we got the OAuth2Token
(which actually deserves an better name like TokenResponse
),
we could get the actual accessToken
of out it, use which to make API requests to resource server (often time same as the authorization server)
Network.OAuth.OAuth2.HttpClient provides a few handy method to send such API request. For instance,
authGetJSON -- Makes GET request and decode response as JSON, with access token appended in Authorization http header. authPostJSON -- Similar but does POST request
In this tutorial, it makes request to auth0UserInfoUri
to fetch Auth0 user information
so application knows who did the authorize.
The end
That's it! Congratulations make thus far!
If you're interested more of OAuth2, keep reading on https://www.oauth.com/, which provides a nice guide regarding what is OAuth2 and various use cases.
Synopsis
- auth0 :: OAuth2
- authorizeUrl :: URI
- randomStateValue :: ByteString
- auth0UserInfoUri :: URI
- data Auth0User = Auth0User {}
- app :: IO ()
- indexH :: IORef (Maybe Auth0User) -> ActionM ()
- loginH :: ActionM ()
- logoutH :: IORef (Maybe Auth0User) -> ActionM ()
- callbackH :: IORef (Maybe Auth0User) -> ActionM ()
- uriToText :: URI -> Text
- bslToText :: ByteString -> Text
- paramValue :: Text -> [Param] -> Either Text Text
- excepttToActionM :: Show a => ExceptT Text IO a -> ActionM a
- oauth2ErrorToText :: OAuth2Error Errors -> Text
Configuration
authorizeUrl :: URI Source #
randomStateValue :: ByteString Source #
You'll need to find out an better way to create state
which is recommended in https://www.rfc-editor.org/rfc/rfc6749#section-10.12
auth0UserInfoUri :: URI Source #
Endpoint for fetching user profile using access token
Instances
FromJSON Auth0User Source # | |
Generic Auth0User Source # | |
Show Auth0User Source # | |
type Rep Auth0User Source # | |
Defined in HOAuth2Tutorial type Rep Auth0User = D1 ('MetaData "Auth0User" "HOAuth2Tutorial" "hoauth2-tutorial-0.1.2-JtzFN92zawBGJl3PTggFZh" 'False) (C1 ('MetaCons "Auth0User" 'PrefixI 'True) (S1 ('MetaSel ('Just "name") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Text) :*: (S1 ('MetaSel ('Just "email") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Text) :*: S1 ('MetaSel ('Just "sub") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 Text)))) |
Web server
Utilities
bslToText :: ByteString -> Text Source #