There are many opinions about the pros and cons of using a stateful security session tracking mechanism (a cookie, in most cases) versus using a stateless bearer token that’s submitted with each request to the server (the prefered model for apps that use APIs). This post isn’t one of those. Here, I’ll share my thoughts regarding a question I was asked by a gentlemen at a client not so long ago regarding how the bearer token should be passed from API consumer to API provider.
In web application SSO, it is typical that a bearer token (e.g. SAML2, JWT, etc.) is used to initially establish a user’s security session on a system, but then a cookie (probably with an opaque value) is used to track the user’s security session. This represents a stateful security session tracking mechanism that was initialized by an SSO protocol.
This post assumes that a bearer token (most likely a JWT token acting as an OAuth2 access token) is cached on the API consumer and passed in every interaction (API call) between the client and server, as is common with modern Single Page Applications (SPA) and Native Mobile Applications.
The difference between these two approaches is this: statelessness employs a bearer token submitted on every API request, while statefulness involves a cookie to track security session state. One of the goals of the REST architectural style is statelessness; a major focus recently has been on how to achieve stateless security with API consumers for use with SPA applications, traditional web applications, or native mobile applications.
The allure of token-based authentication
In our scenario, the advantages of bearer-token-based authentication include (at the API layer):
- It’s stateless
- It’s scalable
- A bearer token, such as JWT, can be validated locally without an external call to the identity provider (IdP)
- Cross-domain access controls can be enabled using Cross-origin resource sharing (CORS)
- Claims are stored in the token (this enables the statelessness of the security context)
- It’s more flexible
- Expiration functionality is built in
- There’s no need to ask users for “cookie consent” or to deal with blocked cookies
- It works better on mobile devices
It is generally recommended that the Implicit Grant (OAuth2) or Implicit Flow (OIDC) be used with SPA applications. These don’t include a refresh token, which is needed to obtain a new access token when it expires. The access token must either be valid for as long as the user’s session must last or an alternative approach must be uses, such as the Authorization Code Grant (OAuth2) or Authorization Code Flow (OIDC) with a public client (check out the discussion in this post for more).
Depending on how long the user must stay logged in, extending the validity of the access token may not be recommended. There are important security implications to doing these things that go beyond the scope of this post.
The token hand-off
To start, we will look at the ways that the access token can be passed from API consumer to API provider. To keep things simple, let’s just assume a simple scenario with an API consumer and an API provider (no API gateway). In many real-world SPA applications, there’s a dedicated API backend that is built on top of a traditional web application framework that likely still uses a cookie to track a stateful security session.
When an API gateway is placed in front of that backend API and proxies traffic for multiple APIs or is outside the control of the API owner, then the access token passing patterns described here should be used.
- Query parameter
- Form parameter
- Message body field
- HTTP header (let’s assume authorization header)
Using query parameters (with an HTTP GET) to pass sensitive values is generally not advised per best practices. Those tend to get logged — especially on older systems — and everybody gets queasy when security-related information is displayed in the browser address bar. It should be noted that if you control the system end-to-end and can ensure no sensitive information is logged, then it could be okay. But, let’s skip this option for now, as end-to-end system control is rare with most mobile applications.
Form parameters could be used, but they don’t work for every type of HTTP operation (GET, for example). Similarly, not every HTTP operation (like, again, GET) has a message body; so, form parameters are not a good option for a general purpose security token propagation mechanism. So, that’s not a workable solution in the general case.
That leaves us with cookies and HTTP headers (let’s assume the the authorization header is being used).
Should you keep tokens in cookies or in local storage?
There are two patterns for client-side storage of bearer tokens: cookies and using HTML5 local storage. If cookies are being used to transmit the bearer token from client to server, then cookies would also be used to store the bearer token on the client side. Likewise, if the authorization header is used to transmit the token, then HTML5 local storage (or session storage) would have to be used to store the bearer token.
Another way of phrasing this discussion is: should the bearer tokens be stored on the client side in cookies or in local storage? The answer to this question will then answer our original question of how best to transmit the token. I’ve heard strong arguments in favor of each, which we will walk through.
There tends to be a misconception that using cookies implies that state must be maintained on the server side. This is not true if all claims needed to recreate the security session are available in a bearer token that is submitted via cookie. For the purpose of this post, I’m assuming that isn’t being done. As a result, there are unique benefits to both headers and cookies/local storage.
In favor of the HTTP authorization header and local storage:
- The relevant specification, RFC 6750, states that bearer tokens (including the OAuth2 access tokens) should be passed between API actors in the authorization HTTP request header (with “bearer “ prepended to the value)
- Local storage cannot be accessed across domains
- Token size (if using JWT) depends on what the browser and server support
In favor of using HTTP cookies:
- Token size (if using JWT) depends on what the browser and server support.
There are a host of potential implications for native mobile apps, traditional web apps, and single page applications (SPAs) that stem from these two approaches. We’ll cover those in an upcoming post.
Image: Flickr Creative Commons/T-eresa