In How to Run the Basic SPA Sample we got some OAuth basics working so let’s take a closer look at HTTP messages used by the sample.
You can follow the same process, using an HTTP debugger such as Fiddler or Charles, to look at the requests on your operating system.
Step 1. Web UI Downloads its Configuration
Step 2. Web UI calls API without a Token
The UI first calls the API without a token, which of course is not optimal, but I want to get 401 handling done early, since it is a key part of using access tokens reliably.
Our API then returns a 401 Unauthorized response with a hint as to the reason why – in this case no access token was supplied:
Step 3. Web UI calls Metadata Endpoint
When the Web UI gets a 401 from the API, it realizes that it has to get a new OAuth access token, so it starts a login redirect – the security code for the redirect is managed by the OIDC Client library.
The first thing the OIDC client does is make a cross origin request to call the Okta metadata endpoint, and we configured Okta access for our web domain earlier:
Our SPA will use the authorization endpoint to request access tokens, and our API will use the introspection endpoint to validate received tokens.
Step 4. Web UI Redirects to Authorization Endpoint
Next the OIDC Client formats a login redirect URL using Open Id Connect / OAuth 2.0 keywords:
Okta will only issue tokens to registered client applications and will always return them on the registered redirect URI.
The standard OAuth flow for an SPA is the Implicit Flow, which is what the above message represents, due to the following query parameter:
- response_type = token
Step 5. SPA Response Types
There are 2 main ways an SPA can use tokens and for now we will use the first option, in order to explain OAuth features gradually:
|token||UI receives an access token but does not know any user details, and has to call an API to ask about the user referenced in the access token|
|token id_token||UI additionally receives an id token as proof of authentication – which can contain user info – and the UI must cryptographically verify the id token|
Later, our Final SPA will use ‘token id_token’ and you should always use that in your solution. It is more secure and protects against certain attacks, which we will discuss later.
Step 6. Understand Login Processing
There are 2 main types of login during an OAuth redirect and for now we are using the first type:
- A default login using your Okta credentials
- A federated login to an external system
Note also that a useful vendor specific session cookie is set, which we will return to in later posts on session management.
Step 7. Authorization Response
As can be seen above, the browser is next redirected back to the Okta Authorization Endpoint, which then issues an access token and redirects back to the SPA:
The full URL is shown below and note that token details are included in the client side URL hash fragment and excluded from server logs.
The OIDC Client Library takes care of security aspects such as validating the returned state parameter, which we might omit if we wrote the code ourselves.
Note that the token has an expiry time of 3600 seconds, after which the UI will have to get a new access token.
Step 8. Avoid Redirect Loops for Failed Login Responses
It is a good idea at this stage to make logins go wrong, to check that your login process will never result in a redirect loop. One way to cause a login error is to send an unrecognized scope to Okta:
We then receive an error response, which is read for us by the OIDC Client library. Note that the OAuth specifications require errors to be returned in error / error_description fields:
Step 9. View Access Token Details
Once our SPA has a valid access token it will send the token to our API. First let’s view it by pasting its text into an Online JWT Viewer.
The data points in the token are referred to as ‘claims’ and the ones of most interest are highlighted:
- sub: The subject claim (user id), which identifies the calling user
- cid: The client id, identifying the calling application
- aud: The API for the token was issued to
- iss: The Authorization Server that issued the token
- scp: Scopes can define high level privileges for the token
- exp: When the token expires
JWTs are digitally signed with a Token Signing Key so that APIs will reject them if they are tampered with. However, they are easily viewable via browser tools, so we must avoid disclosing any sensitive user data in them.
OAuth standards do not require that Access Tokens are JWTs and some vendors will use a vendor specific unreadable format.
Step 10. UI stores Access Token
After login the SPA stores the access token in HTML 5 session storage, so that if the page is refreshed the user does not need to re-login.
Step 11. UI calls API with Access Token
The SPA never tries to read the Access Token and instead blindly sends it to the API in the HTTP Authorization Header.
Note that our SPA has implemented the following behavior which is an essential part of reliably using access tokens in UIs:
- If an API call fails with a 401
- Then get a new access token – once only
- And try the API call again – once only
The UI can never fully know whether the access token is valid, and there are multiple reasons why an API could return a 401, including:
- Administrator revocation of Access Tokens
- Administrator changes to Token Signing Keys
Step 12. API Validates Access Token
When the API receives the token it is the API’s job to validate it, which usually means checking the following:
|Issuer||The token is from our Okta server|
|Audience||The token was issued for our API|
|Digital Signature||The token has not been tampered with since Okta issued it|
|Active||The token has not expired or been revoked|
Rather than implementing these checks, our API uses the OAuth introspection endpoint. To see this request in an HTTP debugger such as Fiddler, restart the API with an HTTPS_PROXY environment variable:
- On Windows run ‘npm run httpDebug’
- On Mac OS or Linux run ‘sudo -E npm run httpDebug’
We can then see the message sent between our API and Okta. Note that with this type of usage the API also never reads the access token directly.
The API checks for a success HTTP status code and also that the Active flag is true. If so the API can read token details from the JSON response.
Step 13. Avoid Redirect Loops for Permanent API 401s
In some scenarios the API could return 401 permanently and this is another scenario where we need to avoid redirect loops. One way to simulate this is to intentionally misconfigure the API:
To use introspection, the API ‘base 64 encodes’ its client id and client secret into the Authorization header and sends them as a Basic credential.
If the credential does not match what is configured in Okta then it will return a 401 introspection response to our API as follows:
In our code sample we return a 500 error to the UI, to prevent the redirect loop, since an unexpected error has happened:
Step 14. API Uses Claims from Token
Once token validation has completed, the API moves on to its business logic, which typically involves:
- Applying additional authorization rules
- Working with entities and saving / loading data
We will discuss API Authorization in further detail shortly, since the API is the main place where you protect your Corporate Assets.
Step 15. API returns Personalized Data
Our API is trivially simple but we return some data based on who the user is, which derives from the ‘sub’ claim in the access token.
Step 16. API returns 401 when the Access Token Expires
The UI will continue to use the access token until it expires 1 hour after login or the user closes their browser.
Eventually the token will expire, when the introspection call will indicate that the token is not active, and our API will return a 401 to the UI:
The user will then be redirected to login again and the cycle will repeat with the new access token.
Where Are We?
Hopefully this helps developers to understand the OAuth technical workflow, though both the UI and API are incomplete:
- User sessions are limited to just 1 hour, which we don’t want
- It is not optimal to use introspection for every API request