What should you do with all of these tokens? How can they be used by your application to ensure that only the correct users get access to data and functionality?
Here’s a diagram of a common grant, the Authorization Code grant, from the start until tokens are obtained.
The Authorization Code grant up to the point where tokens are requested from the token endpoint.
This article will look at the options for storing these tokens.
But first, why use the Authorization Code grant or other grants at all? There are, after all, simpler ways to offload authentication. You could use the direct username and password flows. Why bother with the OAuth dance of redirects?
When you use the OAuth grants, you stand on the shoulders of giants. Many people in the Internet Engineering Task Force (IETF) working group have spent lots of time refining this grant, poking and fixing holes in these flows’ security, as well as documenting and building libraries for them. You also benefit from documents such as OAuth 2.0 for browser based apps, currently being developed, and OAuth 2.0 for native apps.
Using standard OAuth grants to integrate a third party authorization server into your application architecture allows you to leverage these benefits. It also leaves open migration possibilities, should your authorization server fail to meet your needs. (OIDC is another standard which layers identity information onto OAuth grants.)
When using the Authorization Code grant in particular, in addition to the wisdom of the IETF members, you get the following benefits:
If you’ve decided to use an OAuth grant, you need to store the resulting tokens. There are two main options:
The first option is to store the access token and refresh token on the client, whether that is a browser, desktop or native application. Only the access token is presented to APIs or protected resources. The refresh token should be presented to the authorization server, but that workflow will be covered in more detail below. If the refresh token cookie is sent to a resource server, it can be safely ignored.
When using a browser, store these as HttpOnly
, secure cookies with a SameSite
value of Lax
or Strict
.
If you choose this option, the browser, whether a simple HTML page with some JavaScript or a complicated single page application (SPA), makes requests against APIs; the access token is then taken along for the ride.
This works great as long as APIs and the server setting the token cookies live on a domain with shared cookies. For example, the code which gets the tokens can live at auth.example.com
and if you set the cookie domain to .example.com
, APIs living at api.example.com
, todo.example.com
, or any other host under .example.com
, will receive the token.
Storing the tokens as secure, HttpOnly cookies.
When using a native app, store these tokens in a secure location, such as the iOS Keychain or Android internal data. This protects these credentials from any other applications running on your device. Retrieve them and append them to the proper header before making API requests.
In the diagram above, there’s a Validate Access Token
step. Validating the access token when it is presented to securing your application. Each API validates the token presented by the client every time, even if the token has been seen before, as is the case with api.example.com
.
One validation approach that is an option if the token is signed and has internal structure is illustrated below. This is true of a JSON Web Token (JWT) based access token. JWTs are used by FusionAuth and other authorization servers for access tokens, but this is not guaranteed by the OAuth specification.
With a signed token, an API server validates the access token without communicating with any other system, by checking the signature and the claims.
Zooming in on token validation.
The APIs must validate the following:
exp
claim)nbf
claim)aud
claim)iss
claim)This validation should be performed as soon as the request is received, possibly by an API gateway. If any of these checks fail, the requester is essentially unknown. Therefore, the request is from, at best, buggy software and, at worst, an attacker.
The signature and standard claims checks can and should be done with a language specific open source library, such as fusionauth-jwt (Java), node-jsonwebtoken (JavaScript), or golang-jwt (golang).
Checking other claims is business logic and can be handled by the API developer. Again, it’s important that you take this extra step.
If the access token doesn’t meet the criteria above, you can introspect the token by presenting it to the authorization server. With this process, the validity of the token is confirmed by the token issuing software.
Storing the tokens as secure, HttpOnly cookies and using introspection to validate them.
A successful introspection request returns JSON. Claims in this response still need to be checked:
exp
claim)nbf
claim)aud
claim)iss
claim)Using introspection adds a dependency on the authorization server, but removes the need for APIs to validate the token signature.
At some point every access token expires, and the client will, when presenting it to an API, be denied access. The client must be ready to handle this type of error.
When you initially request the offline_access
scope, you will receive a refresh token as well as an access token after a user authenticates.
Using a refresh token.
When the access token expires, the client can present the refresh token to the authorization server. That server validates the user’s account is still active, that there is still an active session, and any other required logic. The authorization server can then issue a new access token. This can be sent to the client and transparently extends the user’s access to the APIs.
If you use client stored tokens, you gain horizontal scalability, since each API can take requests directly from every client. As mentioned above, this approach is a great fit for a single page JavaScript application using data from multiple APIs on the same domain.
Using secure HttpOnly
cookies protects you from cross-site scripting (XSS) attacks. XSS is a common way for attackers to gain access to tokens. When they gain the tokens, they can make requests masquerading as the user for whom the token was granted. Secure HttpOnly
cookies, however, are not available to JavaScript running on the page, and therefore can’t be accessed by malicious scripts.
If your APIs are on multiple domains, or on domains different than what can set a token cookie, you have two options:
Below is a diagram of the proxy approach, where an API from todos.com
is called via a proxy at proxy.example.com
. Cookies set from the .example.com
domain will never be sent to the todos.com
domain due to browser rules.
Using a proxy to access APIs on different domains.
Why use browser cookies as opposed to another storage mechanism such as memory or localstorage? Why not bind the cookie to the browser? All options have tradeoffs, and using cookies works for many applications.
Localstorage is an insecure option because, unless you also set a fingerprint cookie, as recommended by OWASP, you are exposed to XSS attacks. Any JavaScript running on the page has access to localstorage. If you do follow the OWASP recommendations by adding a fingerprint to your token and sending a cookie down with a related value, you are limited to API requests on the domain to which the cookie is scoped, which doesn’t win you much.
If you use an in-memory storage solution, when the browser is refreshed, the token is gone. The user has to log in again; not a great experience.
Another option is a service worker to isolate access to the tokens. This is a good choice, but then all requests from the application must then pass through the service worker. You’re essentially building an in-browser proxy, which may be over-complicated.
Client binding measures, such as Distributed Proof of Possession (DPoP), remove the danger of XSS. A token can’t be used without the private key only the proper client possesses. However, these approaches require additional setup on the client side and are relatively new. As of this writing, DPoP is not yet an IETF standard.
If client storage options don’t meet your needs, another option is web sessions.
You can store the access token and refresh token in the server-side session. The application can use web sessions to communicate with the server. The token is then available for any requests originating from server-side code. This is also known as the backend for frontend (BFF) proxy.
Storing the tokens server-side in a session.
If you need to retrieve data from other APIs with no domain limits, over secure, server-side channels, this is a good option. If you don’t really care about what the token gets you access to, you can examine the claims and validity, then discard it, assured the user has authenticated at the authorization server.
Below is an example of proxying API requests through server-side components. The APIs receiving the tokens still need to validate them.
Proxying API calls using tokens stored in a server-side session.
Even if you don’t use token to gain access to APIs from server-side code, you still get benefits from using the OAuth Authorization Code grant:
What about the id token? That was mentioned above as an optional token, but not discussed further.
The token is delivered when you request a scope of profile
in the initial authorization sequence. After successful authentication, there is an id token as well as an access token provided by the authorization server. There are other OIDC scopes as well, beyond profile
, which can get you access to different user data.
The id token can be safely sent to the browser or client and stored in a relatively insecure location, such as localstorage. The id token should never be used to access protected data, but instead is for displaying information about a user such as their name. Id tokens are guaranteed to be JWTs, so you can validate them client side.
The two options of client-side cookie based token storage or server-side session based token storage handle many systems using OAuth and OIDC to safely authenticate and authorize users.
Client-side storage is a great choice when you have disparate APIs and need to scalably support highly distributed clients such as mobile devices or browsers. Server-side session storage is simpler and easier to integrate into monolithic applications.