Storing OAuth Tokens

By Dan Moore

Storing OAuth Tokens

OAuth grants specify particular flows of formatted data between the various parties, including the authorization server, the client and the resource server. At the end of a grant, one or more tokens are delivered. These tokens are time bound credentials that give access to protected data and functionality.

These tokens include an access token, an optional refresh token (if the proper scope is requested), and an optional id token (if using OpenID Connect).

At a high level, these each serve different purposes.

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.

User/BrowserAppAuthorizationServerWhat Happens Now?View Initial PageClick LoginRedirect User To FusionAuthWith ScopesRequest Login PageReturn Login PageProvides CredentialsRedirect With Authorization CodeRequest Redirect URIRequest TokensReturn TokensUser/BrowserAppAuthorizationServer

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.

Why Use OAuth Grants?

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 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:

Client-side Storage

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.

User/BrowserAppAuthorizationServerapi.example.comhelp.example.com... Proceed Through Authorization Code Grant ...Return TokensSend Tokens As HTTPOnly, Secure CookiesWith a Domain of example.comSend Access Token With RequestValidate Access TokenSend Data or Complete Requested OperationSend Access Token With RequestValidate Access TokenSend Data or Complete Requested OperationSend Access Token With Different RequestValidate Access TokenSend Data or Complete Requested OperationUser/BrowserAppAuthorizationServerapi.example.comhelp.example.com

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.

Validating the Tokens At the Resource Server

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.

User/Browserapi.example.comhelp.example.com... Tokens Have Been Stored In Cookies ...Send Access Token As Cookies With RequestValidate Access TokenSend Data or Complete Requested OperationSend Access Token As Cookies With RequestValidate Access TokenSend Data or Complete Requested OperationSend Access Token As CookiesWith Different RequestValidate Access TokenSend Data or Complete Requested OperationUser/Browserapi.example.comhelp.example.com

Zooming in on token validation.

The APIs must validate the following:

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.

Token Validation With Introspection

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.

User/BrowserAppAuthorizationServerapi.example.com... Proceed Through Authorization Code Grant ...Return TokensSend Tokens As HTTPOnly,Secure CookiesSend Access Token With Any API RequestsValidate Access Token ViaIntrospectionUser Data Provided After AccessToken ValidatedSend Data or Complete Requested OperationUser/BrowserAppAuthorizationServerapi.example.com

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:

Using introspection adds a dependency on the authorization server, but removes the need for APIs to validate the token signature.

Using the Refresh Token Grant

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.

User/BrowserAppAuthorizationServerapi.example.com... Proceed Through Authorization Code Grant ...Return TokensSend Tokens As HTTPOnly, Secure CookiesWith a Domain of example.comSend Access Token With RequestValidate Access TokenSend Data or Complete Requested OperationSend Access Token With RequestAccess TokenDetermined To BeInvalidSends Access Denied MessagePresent Refresh TokenValidate Refresh TokenPresent New Access TokenSend Access Token With RequestValidate Access TokenSend Data or Complete Requested OperationUser/BrowserAppAuthorizationServerapi.example.com

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.

Benefits of Client-side Tokens

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.

User/BrowserAppAuthorizationServerproxy.example.comapi.example.comtodos.com... Proceed Through Authorization Code Grant ...Return TokensSend Tokens As HTTPOnly, Secure CookiesWith a Domain of example.comSend Access Token With RequestValidate Access TokenSend DataSend Access Token With RequestValidate Access TokenSend Access Token With RequestValidate Access TokenSend DataSend DataUser/BrowserAppAuthorizationServerproxy.example.comapi.example.comtodos.com

Using a proxy to access APIs on different domains.

Alternatives To Browser Client-side Tokens

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.

Server-side Token Storage

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.

User/BrowserAppAuthorizationServer... Proceed Through Authorization Code Grant ...Return TokensStore Tokens in SessionReturn Session CookieSession Based InteractionsData/HTML/JSONUser/BrowserAppAuthorizationServer

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.

User/BrowserApptodos.comreminders.com... Token Previously Stored In Session ...Request Todo EndpointRequest Todo EndpointPassing Token From SessionAlong In HeaderValidate TokenSend Todo DataSend Todo DataRequest Reminder EndpointRequest Reminder EndpointPassing Token From SessionAlong In HeaderValidate TokenSend Reminder DataSend Reminder DataUser/BrowserApptodos.comreminders.com

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:

The Id Token

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.

Summing Up

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.