When a public SPA needs to authenticate through OAuth 2.0, it faces a big challenge.
The challenge is you cannot store any type of secrets of credentials in your code. This is why OAuth provided a special flow to be used by SPA: Authorization Code Grant & PKCE. We are going to describe this flow in details here.
Summary of the process:
To describe the flow briefly, when the SPA requires to get a token it will direct the application to an Authorization Server (AS), which will use that server login page and UI, and the user will run the login process on the AS server website, then that server will return back to the original client website using a callback url passing the token to the SPA.
So the client SPA has no idea about the credentials and the client cannot intercept those credentials because the login run on Authorization Server website.
So what is Authorization Code Grant & PKCE
On SPA and mobile apps, because it is open code and can be interpreted by anyone, it is essential to not to exchange the token directly.
So, the laternative is the AS
will return a encrypted code, hence the PKCE, and that encrypted code will be sent again by the client to get the token. And only the client that initiated the first login request will be able to call with code.
PKCE
(pronounced ‘pixy’), provides an encryption mechanism for the communication between SPA and the AS
.
How to configure Auth0
Before we start writing code, we should configure our API and our SPA application on Auth0.
I am not going to show the details on how to do it in Auth0, and assume you know how to do it.
- Create an API in Auth0, and configure its audience to be something like :
https://myapiserver.com"
- Create an SPA application and get its
Client Id
.
From Auth0 we need two values:
- the audience you used to create the API.
- the Client Id for the SPA application.
Details of the OAuth Authorization Code flow:
Here are the steps to achieve getting token from Authorization Code flow.
Step 1: call authorize endpoint
The process start when the SPA application will find it needs a token to access the remote resource, so it start the process by navigating to the Authorize end point: https://<your-tenant-id>.us.auth0.com/authorize
passing the following information to this end point:
1GET /authorize?
2response_type=code
3& client_id=<client_id>
4& state=<state>
5& scope=<scope>
6& redirect_uri=<callback uri>
7& resource=<API identifier>
8& code_challenge=<PKCE code_challenge>
9& code_challenge_method=S256 HTTP/1.1
10Host: authorizationserver.com
The parameters are as follows:
- client_id: it is the application (SPA) you create on Auth0.
- audience: is the audience used in configuring API in Auth0. Other OAuth providers use the term
resource
instead of audience, but it play the same function. - redirect_uri: the callback URL to your application. This call back should be one of the allowed url registered in your SPA Auth0 application.
- scope: this setting could be use to define the scope or permissions requested by the client. In OpenID Connect it can be only the value
openid profile
, and in OAuth 2.0 usually it is the list:openid profile email offline_access
. - response_type: should be “code” because Authorization Code flow * response_mode: should be “query” which means the code will return as query parameter.
- code_challenge: a value generated by PKCE code verifier to protect the code. code_challenge_method: S256, it is the recommended encryption to use.
- state: is an encrypted auto-generated string used to track calls during authorization process.
Step 2:
The Authorization Server find there is no valid token for this session, so it redirect it to its own login UI page:
1GET /login?state=<state>
Step 3:
You provide your credentials in the login screen, and the Authorization server might take you to a consent page.
After Submit
login, the page will do a post call to login as follows:
1POST /login?state=<state>
Inside the post there is a form of the credentials:
- username
- password
Step 4:
The Authorization server will call back your SPA with the callback url you provided with the code as query parameter
1HTTP/1.1 302 Found
2Location: https://clientapplication.com/callback?
3code=<authorization code>
4& state=<state>
Last Step Step 5: generate token
Last step is you call the Authorization Server token
endpoint with POST
with the authorization code that generated from pervious step:
1POST /token HTTP/1.1
2Host: <auth0 tenant authorization url>
3Content-Type: application/x-www-form-urlencoded
4grant_type=authorization_code
5& code=<authorization_code>
6& client_id=<client id>
7& code_verifier=<code verifier>
8& redirect_uri=<callback URI>
The server will response with json with the token and refresh token, and maybe id_token (in case of OpenID Connect):
1"access_token":"<refresh token>",
2"token_type": "Bearer",
3"expires_in":<token expiration>,
4 "refresh_token":"<refresh_token>"
5"id_token": "<id token>",
6"scope": "openid profile email"
7}
How to use Auth0 JS client:
As I meantioned before, if you use an off-the-shelf authentication service, it will hide the complexity for you, and you don’t have to work with previous http workflow.
From the Auth0 JavaScript client: Auth0-SPA-JS.
You have to do only four steps:
- create an auth0 client:
1import { createAuth0Client } from '@auth0/auth0-spa-js';
2
3//with async/await
4const auth0 = await createAuth0Client({
5 domain: '<AUTH0_DOMAIN>',
6 clientId: '<AUTH0_CLIENT_ID>',
7 authorizationParams: {
8 redirect_uri: '<MY_CALLBACK_URL>'
9 }
10});
- call login, with will do authorize / login API call
1 await auth0.loginWithRedirect();
- in the redirect url, write a handler:
1 const redirectResult = await auth0.handleRedirectCallback();
2 //logged in. you can get the user profile like this:
3 const user = await auth0.getUser();
- get the token:
Any time you need to call an API to authenticate you can get the token
1var accessToken = await auth0.getTokenSilently();
2
3var result = await fetch('<my api url>', {
4 headers: {
5 Authorization: `Bearer ${accessToken}`
6 }
7});
8
9var data = await result.json();
A word about scopes:
An API can define a set of permissions that can be used to divide the functionality of that API into smaller chunks. When a API’s functionality is chunked into small permission sets, third-party apps can be built to request only the permissions that they need to perform their function. Users and administrators can know what data the app can access.
In OAuth 2.0, these types of permission sets are called scopes, and they are presented as string values.
The values can be something like this as examples: financial:read, financial:write, admin, user:write…
For the simpler protocol OpenID Connect, it uses only three predefined scopes:
- openid: indicate the request could be used in OIDC for authentication. It will return the user ID in
id_token
, orsub
- profile: return profile information like first name, last name, …
- email: return the email