Previously, I examined OAuth at a high level, including the standards and the grants outlined in the specifications. If you haven’t you may want to read that article. In this post, you’ll learn the nuts and bolts of integrating OAuth to protect resources like APIs.
While there are a number of different ways to do so, if a user is involved, the Authorization Code grant is the recommended choice. Let’s take a deeper look at the Authorization Code grant and how it can be used to protect your API.
For an example application, let’s use a todo application. Below is an architecture diagram of this system. The Authorization Code grant requires an additional architectural component beyond what you might expect, here termed the OAuth token exchange component because one of the main services it performs is procuring the access token via an exchange with the OAuth server.
Below I’ll cover the flow of the request in great detail, but first, let’s talk about responsibilities. There are three main parties responsible for the components in this diagram:
- You, the application developer, are responsible for the client application. You are also responsible for the todo API, which stores todos and makes them available, typically via a JSON API over HTTP. The former will be a mobile application and the latter will live at todo.example.com.
- The OAuth and User management platform is typically going to be a third-party provided component, whether commercial or open source, SaaS or self-hosted. There are many solutions, including Auth0, Keycloak, and FusionAuth (full disclosure, I am a FusionAuth employee). This is the component whose documentation and standards support I encouraged you to review above. This server will live at auth.example.com
- The OAuth token exchange component can be written by the application developer. But it is more commonly implemented through an open-source library or as part of a framework. PHP’s oauth2-client or Microsoft’s OpenIdConnect classes are examples of the latter. This component will live at app.example.com.
One time setup
Installing the OAuth server
To use OAuth, you need to get the OAuth server up and running. This is out of the scope of this article, but is a step that should not be neglected.
Configuring the OAuth server
Any applications interacting with the OAuth and User management platform need to be configured in that platform. While there is some support for dynamic client configuration, which is standardized in RFC 7591, for the purposes of this article, let’s walk through a manual configuration. Note that the specifics are highly implementation dependent, but here’s a sequence diagram showing the typical steps:
There is an admin user who configures the OAuth server for the new client. This may be done via UI or API.
The nomenclature varies for each OAuth server, but this entity might be called an application or a client. In particular, you are looking for something called the client_id and the client_secret. You also will be looking to configure a redirect_uri.
The client_id and client_secret may be automatically generated for you. You’ll want to keep the client_secret, well, secret. The client_id can be shared with the client, but the OAuth token exchange components should have both the client_id and the client_secret.
You’ll need to provide the redirect_uri. That will be a URL pointing to the OAuth token exchange component. As mentioned above, the client will be redirected there after successful user authentication, so it must be a location accessible to that client. It can be a public facing URL, a web app on an internal URL, or even a deep link into a mobile application.
Here, the redirect_uri will be https://app.example.com/oauth-callback but of course in your implementation it will be different. Below, we’ll build out the code that will exist at that endpoint.
You may need to configure custom scopes as well. These are string representations of permissions the user can grant to the client at the time of authentication.
This configuration is typically done once and rarely updated.
Enable CORS
This is going to be a browser-based application. Make sure you enable and configure CORS so that JavaScript served from app.example.com can access todo.example.com.
Now let’s look at a more typical sequence which will happen every time a user authenticates and then accesses the Todos API.
Flow of requests
I am going to dive into detail of each of these requests below, but it can be helpful to have a high level understanding of what will happen.
- The client requests a page containing a login link to the OAuth server living at auth.example.com.
- The user clicks on the link.
- The client requests a page from the OAuth server which contains a form to gather login information, such as a username and password.
- The OAuth server authenticates the user.
- The OAuth server sends the client an authorization code, which looks like “SplxlOBeZQQYbYS6WxSbIA”. This is one time use. The code is sent in the URL of a redirect. What is the target redirect URL? It is the OAuth token exchange component living at app.example.com.
- The OAuth token exchange component has been configured to communicate directly with the OAuth server and has credentials that will uniquely identify it. These were set up previously. It presents that information along with the authorization code to the OAuth server.
- The OAuth server validates the code and other parameters, including the client ID and client secret. If it all checks out, mints a time bound access token, cryptographically signing it, and returns it to the OAuth token exchange component. This is typically valid for seconds to minutes.
- The OAuth token exchange component can store the token in the session or send the token to the client. Let’s assume the latter: the access token is sent down to the client as a secure HTTP-only cookie. (There are other patterns of storing the token which are beyond the scope of this article.)
- The client makes an HTTP request to the Todo API at todo.example.com. Because it is an HTTP request and the Todo API and the OAuth token exchange component are on the same domain (example.com) the token is sent along with the request.
- The Todo API receives the token and validates it by:
- Checking the signature to make sure the token wasn’t modified in-flight.
- Confirming the token is not expired and hasn’t been issued for a time in the future.
- Checking to see that the audience, issuer, and other token claims are correct.
- If the token checks out, the Todo API accesses the datastore and returns the user’s todos.
- The client displays the todo data.
- As long as the client has a valid access token, it continues to make requests of the Todo API without communicating with the OAuth server.
- Eventually the access token expires. The client doesn’t receive data, but instead an error code.
- At this point, the client can choose to call the /refreshtoken endpoint on the OAuth token exchange component.
- The backend forwards the request on to the auth server.
- The auth server processes the refresh token request. If the refresh token is valid, it responds with a new access token.
- The application then sends the new access token down to the client as a cookie, again.
- The client makes another request to the Todo API with the new token.
- The Todo API returns the todos.
At this point you should have a high level understanding of the flow.
Let’s dig into each of these steps, showing you code where applicable.
Beginning the login process
The very first step in the Authorization code grant is to have the client, whether browser or native app, provide a link to the OAuth server. For tidiness, let’s encapsulate it in an express route; the HTML will look like this:
<a href='https://app.example.com/login'>Login</a>
And the code for the login route will look like this:
const pkceChallenge = require('pkce-challenge'); // use this NPM module
router.get('/login', (req, res, next) => {
const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
res.session.state = state;
const pkce_pair = pkceChallenge();
req.session.verifier = pkce_pair['code_verifier'];
const codeChallenge = pkce_pair['code_challenge'];
scope= ‘offline_access’;
const codeChallenge = generateAndSaveCodeChallenge(req, res);
res.redirect(302,
config.authServerUrl + '/oauth2/authorize?' +
`client_id=${clientId}&` +
`redirect_uri=${redirectURI}&` +
`state=${state}&` +
`response_type=code&` +
`scope=${scopes}&` +
`code_challenge=${codeChallenge}&` +
`code_challenge_method=S256&`);
});
We’re doing a 302 redirect because it keeps the URLs in the app looking clean, but it isn’t required by the OAuth flow.
Let’s walk through each of these parameters.
We are going to create a random state value and store it in our session. This will help prevent CSRF attacks. In particular, from RFC 6819, a best practices security RFC, using state prevents a situation “where an attacker authorizes access to his own resources and then tricks a user into following a redirect with the attacker's token.“
We’re using PKCE, which requires us to generate both a secret and a hash of a secret. We’re storing the secret in our session as well.
Then we build the redirect statement.
This conveys all the information our IdP needs to properly authenticate the user and send the client to the OAuth token exchange component.
The authServerUrl is the known location of the OAuth and user management server.
We provide the client_id, which identifies the client previously configured. We also provide the redirect_uri, also previously configured, and is where the OAuth server will send our users after they have successfully logged in. As mentioned above, that is https://app.example.com/oauth-callback for this example.
The state parameter is part of the URL, and, as mentioned above, will be used to prevent CSRF attacks.
- The response type is always going to be “code” for any Authorization Code grant. This is a parameter specified in the RFC and tells the OAuth server what kind of grant this request flow is using.
- Next up is the scopes parameter which indicates the type of data the client is requesting. These can be standard ones like offline_access or custom ones for your application. Here, we request offline_access which will get us a refresh token to use in the refresh token grant. The user may be prompted to accept these scopes by the OAuth server.
- Finally, the PKCE code challenge parameters send the hash of the PKCE secret to the OAuth server and specify the hashing algorithm used. The latter is almost always S256.
The next step is for the OAuth server to render a login form based on these parameters. Then the user can enter credentials.
Entering credentials
This is where the rubber meets the road. The user finally has a chance to authenticate. The user experience depends on each IdP implementation, but may take the form of a login screen, like this:
There could be other authentication factors required, like:
- a one time code
- a time-based one time password (TOTP)
- or a biometric identification provided using the FIDO2 standard
Your application doesn’t have to care about the specific details of that authentication.
Once the user is authenticated to the satisfaction of the OAuth server, it does a redirect.
The authorization code is sent to OAuth token exchange component
Remember the redirect_uri that was configured before the Authorization Code grant started and sent as a URL parameter when the browser was redirected to the OAuth server?
Well, that URL now comes into play. The code living at that URL receives a number of parameters from the OAuth server. It won’t receive them directly, but rather through the browser.
The OAuth server will construct the URL with a base of the redirect_uri ( https://app.example.com/oauth-callback as configured above) and some additional parameters so that the final URL looks like this:https://app.example.com/oauth-callback?code=+WYT3XemV4f81ghHi4V+RyNwvATDaD4FIj0BpfFC4Wzg=&state=M1TkQB5O3bxKTJSWZAy8
Let’s look at each of the parameters in this URL:
- code: this is a one time code associated with the authentication event.
- state: this is the value we provided when we originally built the login URL. It is echoed back to us by the OAuth server. Again, this is primarily used for CSRF protection.
The OAuth token exchange component which is running at the URL should process these three parameters, verify them as needed, and then contact the OAuth server directly to receive an access token.
Here’s example code to implement this:
const axios = require('axios');
router.get('/oauth-callback', (req, res, next) => {
// Verify the state
const reqState = req.query.state;
const state = req.session.state;
if (reqState !== state) {
res.redirect('/', 302); // error, CSRF attack detected. Don’t get a token
return;
}
const code = req.query.code;
const codeVerifier = req.session.verifier;
// POST request to Token endpoint
const form = new FormData();
form.append('client_id', clientId);
form.append('client_secret', clientSecret)
form.append('code', code);
form.append('code_verifier', codeVerifier);
form.append('grant_type', 'authorization_code');
form.append('redirect_uri', redirectURI);
axios.post(config.authServerUrl+'/oauth2/token', form, { headers: form.getHeaders() })
.then((response) => {
const accessToken = response.data.access_token;
handleTokens(accessToken, refreshToken, res);
}).catch((err) => {console.error(JSON.stringify(err));});
});
This application fragment checks to see that the state was what we previously had set, then it builds a request to the OAuth server.
The request includes many parameters we’ve seen before:
- The client_id and client_secret: these are the identifiers configured when we set up the initial client configuration in the OAuth server. These uniquely identify the application to the OAuth server.
- The code: this identifies the authentication event, when the user authenticated at the OAuth server.
- The code_verifier: this is part of PKCE and is the unhashed random value. The OAuth server will compare the hashed value it received on the initial login with a hash of this value. If they differ, the token will not be issued.
- The grant_type which is always "authorization_code".
- The redirect_uri, which was present on the very first request of this grant.
These parameters are posted to the token endpoint. The location of that endpoint is not standard, but is typically specified in the OAuth server documentation.
If successful, the request ends up with the access_token being delivered. We’ll talk about how to deliver it in the next section, but this access token will be presented to the Todo API by the client.
OAuth token exchange component sends token to client
Let’s talk about the handleTokens method called above.
There are a number of ways to secure this access token. Remember, this token will grant access to the Todo API and is not bound to the client in any way.
It’s a bearer token. Just like a car key, anyone who has this token can use it. So we want to keep it safe.
Depending on your security posture, you could:
- Store this token server side in the session and have all access to the Todo API be proxied through this server-side code. This is known as the BFF pattern.
- Send the token down to the client to be stored securely. If on a native app, use the iOS keychain or Android secure storage. If in a browser, then store the token in an in-memory variable or as a secure, HTTP only cookie.
What you shouldn't do is send the token to the client and store the token in any place accessible to other code, such as the browser local storage.
Let’s send the tokens down as a secure HTTP only cookie. Because of cookie domain limitations, the Todo API will now have to share a domain with app.example.com, but luckily we can set the domain to example.com.
function handleTokens(accessToken, refreshToken, res) {
// Write the tokens as cookies
res.cookie('access_token', accessToken, {httpOnly: true, secure: true, domain:’example.com’});
res.cookie('refresh_token', refreshToken, {httpOnly: true, secure: true, domain:’app.example.com’});
res.redirect(‘/’);
}
At this point, the application running in the browser has a valid access token. Any requests to any servers in the example.com domain will receive the access token.
However, only the app.example.com API will get the refresh token. When the Refresh grant is discussed below, you’ll see why.
What happens next? Remember the todos that we wanted to get to render? Let’s request those.
Client provides token to API
The client needs to request the API from todo.example.com. How you do this exactly depends on the particular JavaScript library, but here’s an example:
axios.get(‘https://todo.example.com/todos’, { withCredentials: true })
.then((response) => {
if (response.status == 200) {
// … display the todos.
}
}).catch((err) => {console.error(JSON.stringify(err));});
Make sure to include the credentials (the cookie containing the access) with our request.
But what should the Todo API do once it receives the token?
It needs to validate it and get the information from the token to find the todos for the user. This is a critical step in the Authorization Code grant that is often but not always partially handled by a library.
API verifies token
When the Todo API receives the access token, it should always verify the token. This is good to do in an API gateway if present, but can be done by each individual API if you’d prefer.
There are two steps to verifying the token:
- Ensuring the token was created by the OAuth server
- Verifying information represented by the token (the claims)
There are two ways to ensure the token was created by the OAuth server and they are both standards (the extensibility of OAuth cuts both ways!).
If your OAuth server supports Introspection (RFC 7662, mentioned above), then you may make a request to the OAuth server. If the token is valid, you’ll receive back claims as JSON, including information about whether the token is active or not.
Another option, if the token is self-contained and signed, is to examine it within the Todo API code. This approach is often taken when the access token is a JWT. You must validate the signature; this is best done with a library. Here’s an example of how to do it with thejsonwebtoken open-source library:
const token = getTokenFromCookie(req); // retrieve the token from the cookie storage
const jwt = require('jsonwebtoken');
const options = {algorithms: "HS256", ignoreExpiration: false, issuer: "fusionauth.io"};
const verified = jwt.verify(token, hmac_key, options);
Verifying the signature is critical because if you don’t, anyone can create a token with any claims in it. This could, for example, authorize the token holder to hold super-admin privileges. All your todos would be exposed to them!
Once you’ve verified the signature, either via introspection or by examining the signature of the token, you still have more validation to do. You need to check the claims.
At the end of the signature verification, you end up with JSON that looks like this:
{
"email": "test@example.com",
"exp": 1643751681,
"iss": "fusionauth.io",
"sub": "test@example.com",
"aud": "myapp.example.com",
"iat": 1643748081
}
With introspection, it will include an active claim:
{
"email": "test@example.com",
"exp": 1643751681,
"iss": "fusionauth.io",
"sub": "test@example.com",
"aud": "myapp.example.com",
"iat": 1643748081,
“active”: true
}
The Todo API must verify the following claims. Some of this verification may be performed by the library you used to check the signature; consult its documentation.
- The active claim, if using introspection. If present, it must be true.
- The iss claim: this can be any kind of unique identifier (domain name, URL, UUID, etc) but must be something the OAuth server provides and the Todo API expects. This value represents the issuer of the token: the OAuth server. The Todo API should know what entity is issuing the token and ignore any tokens (and their requests) that don’t match.
- The aud claim: this can be any kind of unique identifier so long as the OAuth server and the Todo API agree on the format. This may be an array of unique identifiers but is often a single string. This value represents who this token is for; that is, who should consume this token. In this scenario, it is the Todo API.
- The exp and nbf claims. Access tokens are time-bound. These two claims specify when a token expires (exp) and the time before which it is not valid (nbf). These claims are often checked by libraries.
Here’s code showing the checking of the audience claim:
// addl verification checks
if (token.aud != 'todo.example.com' || token.aud.indexOf(‘todo.example.com’) == -1) {
throw "invalid audience";
}
Return the data
At the end of the day, the client is looking for todos for a given user. A user is often represented by the sub claim in an access token; sub stands for subject. Here’s code to look up the todos for a given subject:
function getTodos(subject) {
// look up todos in the datastore, convert to JSON
}
This function could access anything: a database, a flat file system, a NoSQL database. The actual implementation doesn’t matter, only that this data is not accessed before the claims checking above occurs.
Calls against the Todo API will likely happen a number of times. However, eventually the access token will expire. What happens then?
Catch the forbidden error and request a new access token
Access tokens must be validated every time they are received. Because they are time-bound, they will eventually expire. At that point, the request from the client will receive a non-200 status code. It may be a 400 or 403, depending on your API design. Let’s modify the error handling logic:
axios.get(‘https://todo.example.com/todos’, { withCredentials: true })
.then((response) => {
if (response.status == 200) {
// … display the todos.
}
.catch(buildAttemptRefresh(getTodos));
Request the /refreshtoken endpoint
When this code gets an error, it attempts to provide a refresh token to an endpoint at app.example.com.
buildAttemptRefresh looks like this:
const buildAttemptRefresh = function(after) {
return (error) => {
console.log("trying to refresh");
// try to refresh if we got an error
axios.post('/refreshtoken', {})
.then(function (response) {
after();
})
.catch(function (error) {
console.log("unable to refresh tokens");
console.log(error);
window.location.href="/";
});
};
}
We’ll also add getTodos, which just requests the todos. This allows us to re-request the todos after we have a refresh token.
const getTodos = function() {
axios.get(‘https://todo.example.com/todos’, { withCredentials: true })
.then(function (response) {
buildUI(response.data);
buildClickHandler();
})
.catch(console.log);
}
This code goes to app.example.com (the OAuth token exchange component) instead of auth.example.com (the OAuth server) because the OAuth token exchange component can proxy the request to the OAuth server, but also modify the response. If the refresh token request succeeds, the OAuth token exchange component can send the access token back down as a secure cookie.
If the client pursued the Refresh grant directly, the OAuth server following the standards would return the access token, which would expose it to JavaScript.
This is also why the Refresh token cookie had a more limited domain in the handleTokens method. Even if the client tries to send the refresh token cookie to the auth server, the cookie rules around domains would prevent it.
Proxy the request to the OAuth server
The OAuth token exchange component has the client ID and secret, so can make a proper Refresh grant request.
However, this needs the refresh token. The client has provided it in the cookie, so the code can extract it from the request. Then, using these values, it will build the grant request.
router.post('/refreshtoken', async (req, res, next) => {
const refreshToken = req.cookies.refresh_token;
const form = new FormData();
form.append('client_id', clientId);
form.append('grant_type', 'refresh_token');
form.append('refresh_token', refreshToken);
const authValue = 'Basic ' + Buffer.from(clientId +":"+clientSecret).toString('base64');
const response = await axios.post(config.authServerUrl+'/oauth2/token', form, {
headers: {
'Authorization' : authValue,
...form.getHeaders()
} });
const accessToken = response.data.access_token;
// … return to the browser
}
Return the new token to the client
Finally, the section
// … return to the browser
Must be implemented.
If delivering the access token as a cookie, this will look like:
res.cookie('access_token', accessToken, {httpOnly: true, secure: true});
After this, the client has a new access token with a future expiration time. This token can be used to retrieve more todos from the Todo API.
Conclusion
In this article, you learned about the Authorization Code grant and why you should use it. You also saw a step by step examination of how an Authorization Code grant would work, including code samples.
Using this grant gets your code a time limited credential (the token) which is affiliated with a user, but has no direct connection to their credentials. There’s additional flexibility as well: the OAuth server can change preferred authentication methods without modifying the API consuming the token.
While the process to obtain and use a token can seem complex, by using OAuth and the Authorization Code grant you are leveraging the work of standards bodies and experts to protect your APIs and other resources.