Spotlight on the CIAM Token Storage Conundrum

Tokens lie at the heart of modern CIAM integrations, enabling scalable and secure authentication and authorization. However, their power makes them a target for attackers. Mismanaged tokens and token storage can undermine the security of even the most advanced identity systems, leaving applications and security-sensitive data vulnerable to attack.

Table of Contents

Reading Time: 16 minutes

From a CIAM perspective, a B2C/B2B SaaS application will typically deal with a number of different Tokens, each used for a different purpose and each expressed in a particular format. An ID Token, for instance — used by an application to identify an authenticated user and build the user session — is expressed as a JWT. An Access Token, on the other hand — used when calling an API on behalf of a user — is more often than not expressed as a JWT, but could equally be an opaque artefact requiring the use of Token Introspection. Whilst a Refresh Token — used primarily to recreate an expired Access Token without user interaction — is typically opaque.

Each token has a purpose that is well-defined. But how should one go about storing each of them in a way that ensures safety and security? Should they all be treated in the same manner? Or should some be considered more precious than others? As vehicles for identity and data access, tokens are attractive targets for malicious actors; how and where they are stored, then, becomes a crucial aspect of a secure CIAM implementation. Earlier this year, I came across the following post on X/Twitter (whichever your preference 😉) that sparked some debate. I often find it amusing how the various 3rd-party SaaS CIAM vendors go back and forth with each other as each one vies for their position as the superior solution. However, it does highlight an interesting point: what is the best approach to adopt when it comes to token storage, and which strategy offers the most protection long-term?

Whilst Colin was talking specifically about browser-based scenarios — which are arguably the most challenging — SaaS solutions typically incorporate the likes of Mobile Apps, as well as Backend services too (whether as part of a regular Web Application, or as a BFF in some SPA context or the like). So there are typically multiple scenarios, in multiple different contexts, in which token storage needs to be considered. My name’s Peter Fernandez, and in this article, I’m going to take a look at the various token storage options available and how they compare and contrast, in an attempt to shine a spotlight on the particular aspect of token safety and security that I like to refer to as the Token Storage Conundrum.

In-Memory Storage

In-memory typically refers to a situation where a token is stored in a non-persistent manner within a (language) runtime itself; in the case of JavaScript, for example, this means using the built-in dynamic object data structure in which to store values. With a lifetime that only extends as long as the duration of the execution context, in-memory can be considered the most secure mechanism for token storage because a token is not really “stored”: storing tokens typically referres to a persistent manner, and in the Browser, for example, in-memory storage typically means until a page refresh occurs. In a Backend, until a request is complete.

Returning to the “tweet” Colin made above, in their latest documentation — at least at the time of writing this article — Auth0 (https://auth0.com) largely recommends in-memory storage as the best option for a Browser context, particularly where a Web Worker is also utilised; see below for more details. Noting a few caveats for consideration. Of course, the downside is that once an execution context expires, all traces of the token are erased — a good thing from a security perspective, but also potentially problematic:

  • For an ID Token, this isn’t too much of an issue, as typically an application will use the ID Token to establish the authenticated user session and then discard it. However, depending on your IdP of choice, and also your preferred mechanism for Logout — i.e. Logout from the IdP as well as the application — an ID Token, regardless of its expiration, may need to persist for the duration so that it can be passed via id_token_hint as recommended in the OpenID spec.
  • For an Access Token that is used as part of a security-sensitive operation — such as transferring a sum of money or the like — in-memory storage is the preferable option. Such tokens should be short-lived anyway, ideally used just once (as in to make one API call), and then discarded once used. For less security-sensitive operations, however, having an Access Token that hangs around for longer (e.g. 24 hours, say) will typically be more efficient, allowing use as part of multiple API calls without needing to make corresponding calls to the Authorization Server (i.e. to get a new token to call the API).
  • For a Refresh Token, however, in-memory storage is essentially a non-starter. Holding a Refresh Token implies that an application would want to generate new instances of a token at some stage, without user interaction or potentially the user being present at all. The use of persistent storage, then, is key when it comes to utilising a Refresh Token.

From a security point of view, in-memory storage largely relies on using obscurity from a symbolic reference perspective, so one can argue that in-memory is more “security by obscurity”: there’s nothing typically stopping malicious access, it’s just that if malicious code doesn’t know where something is, it can’t find it. In the case of a JavaScript public client, for example, a script isn’t necessarily prevented from accessing a token held in memory; rather, it’s the fact that a symbolic reference to the token is not well-known which makes it harder for a malicious script to find.

Web Workers

A Web Worker (see the Mozilla documentation here for more details) is a mechanism for web content to run scripts in background threads: specifically, a Web Worker executes in a separate Browser context where the global window is unavailable for reference. Thus making it even harder for a malicious script to hunt down a token via some symbolic reference mechanism. Dedicated workers, represented by a DedicatedWorkerGlobalScope object, can be used to provide sandboxing, as storage is only accessible from the script that first spawned it. In contrast, shared workers, denoted via the use of SharedWorkerGlobalScope, provide storage that can be accessed from multiple scripts. The OWASP HTML 5 Security Cheat Sheet here has additional information on the use of Web Workers, including some additional things to consider.

Local Storage

Typically associated with Browser execution i.e. embedded browser execution as well as execution via an externalised chrome localStorage allows data to be stored in key/value pair fashion on a public client, where data persists even after the browsing session has ended (i.e. data remains available for future browsing sessions irrespective of whether a browser window or a browser tab is closed). With local storage, there is no automatic expiration. Underlying storage mechanisms typically vary from browser to browser, as well as platform to platform, and with local storage, authentication at the application-level can easily be bypassed: a script executing on behalf of a user who has local privileges will typically have access to local storage. As discussed in the OWASP HTML 5 Security Cheat Sheet here, the recommendation is that local storage be avoided where sensitive information is concerned.

Local Storage is particularly susceptible to what is commonly referred to as Cross-Site Scripting attacks (or XSS for short; see the OWASP Cross Site Scripting Prevention Cheat Sheet for more details). Since it was originally coined, the term has widened to include the injection of basically any content; however, in the context of local storage, an XSS attack typically focuses on malicious script injection in order to steal token information. Stolen tokens can then be used for account impersonation, observing user behaviour, loading external content, stealing sensitive data, and more.

As local storage is arguably the most vulnerable, OWASP advises that any data stored should also not be trusted. Unlike when using the Path attribute of an HTTP Cookie (discussed below), with local storage, there is also no way to restrict the visibility of an object; every object in local storage is shared within an origin protected by the Same Origin Policy rather than any specific path restriction. So the recommendation is also to avoid hosting unrelated applications that use local storage on the same origin…as all of them would share the same localStorage:

  • For an ID Token, using local storage means that Personally Identifiable Information (PII) could potentially be leaked, particularly where email and/or profile were included as a scope when an authentication request was sent to the IdP (see the OpenID spec here for more details). As local storage is always accessible by JavaScript, the recommendation is to also avoid using it to store any application-level session information.
    • Ideally, a Cookie using the httpOnly flag, as discussed below, would be the preferred choice for storing anything application-level session-related, particularly if an ID Token must be stored for the duration (see discussion above). However, in a SPA (Single Page Application), the choice between using a Cookie or local storage is arguably an arbitrary one, unless a BFF — Backend For Front-end — pattern is also being employed.
  • For an Access Token, the use of local storage is best avoided, particularly where the token is being used as part of any security-sensitive operation (such as transferring a sum of money or the like). Due to the lack of adequate protection, storing Access Tokens of any nature in local storage could cause issues. When you consider that some 3rd-party services will also honour any access tokens they issue when calling the userinfo endpoint, even when not in JWT format, an (opaque) Access Token can still potentially leak PII!
    • Again, a Cookie using the httpOnly flag (discussed below) would be the preferred choice for storing a longer-lived Access Token. However, for a SPA, the choice between using a Cookie or local storage is again an arbitrary one, unless a BFF pattern is also being employed. As previously discussed, in-memory should still be the preferred choice where Access Tokens for security-sensitive operations are concerned.
  • For a Refresh Token, local storage should ideally never be used. A Refresh Token can be used to acquire new tokens without user interaction, so putting a Refresh Token in local storage would be semantically equivalent to making user credentials readily accessible, albeit for a specific scope of resource access.
    • Some 3rd party SaaS CIAM solutions, such as Auth0, recommend using Refresh Tokens in a SPA context to mitigate problem scenarios involving Intelligent Tracking and Prevention (a.k.a. ITP2) and the like; see the Auth0 article here for further details. Some would argue that this is due to session architecture limitations associated with the solution itself rather than anything else. ITP2, or similar, may not be an issue for you, but if it is and you are building a vanilla SPA — as in one where a BFF is not being used — then local storage is your only option.

Session Storage

Whilst similar in its implementation, the difference here is that while local storage is partitioned by origin only — i.e. utilising Same Origin PolicysessionStorage is partitioned by both origin and top-level browsing context. Thus, the data in session storage is only kept for the duration of a session, i.e. only persists for as long as the browser or browser tab with which it’s associated remains open. Again, whilst there is no automatic expiry per se, at least the data contained in session storage doesn’t stay around forever. Session storage is also isolated, so in the case of the browser, each tab has its own separate sessionStorage block.

Cookie Storage

Designed as a way for websites to easily and conveniently store limited amounts of state information at scale, an HTTP Cookie (also referred to as a Web Cookie, Internet Cookie, Browser Cookie, or simply Cookie for short; see here for more details) is a small block of data typically created by a web server while browsing a website. Cookies are most commonly associated with a Browser context, and whilst also stored on the user device where browsing occurs, there are several notable differences between a Cookie and Local Storage:

  • a Cookie is more often associated with a regular Web Application than with a SPA.
  • a Cookie is included as part of HTTP request/response communication
    • typically as part of the HTTP Header.
  • a Cookie can have an associated Expires, or Max-Age, so that explicit expiry occurs in an automated fashion
  • a Cookie Path attribute indicates a URL path that must exist in the requested URL in order to send the Cookie.
    • the optional Domain attribute can also be used to specify on which server the Cookie can be received.
  • a Cookie with the Secure attribute will only be sent to the server with an encrypted request over the HTTPS protocol.
  • a Cookie with the HttpOnly attribute can’t be accessed via JavaScript; more on this below

A cookie can be created by the backend server as well as the front-end client; data in local storage, on the other hand, is only ever created by the front-end. The exception to this is where the HttpOnly attribute is used; discussed in more detail below. For a SPA (Single Page Application), the preferred HttpOnly attribute can’t be used unless a BFF — Backend For Front-end — pattern is also employed. So, for a vanilla SPA (as in one that doesn’t employ a BFF), there is arguably little difference between using a Cookie vs using local storage.

Unlike local storage objects, cookies are transmitted between the client and the server with each HTTP request/response, typically as part of the HTTP header; local storage objects only exist on the browser, though, like local storage, a Cookie sent to the browser can potentially be interacted with from a JavaScript perspective (given the right conditions). The additional attributes that can be used to enforce control over a Cookie — i.e. Max-Age (Expires), Path, Secure, and HttpOnly in particular — arguably make it a generally safer option when it comes to token storage, but only if they are/can be used:

  • For an ID Token, the choice between local storage and a Cookie is marginal, at least for a vanilla SPA. If an application has to store the ID Token for the duration (as discussed in the previous section), using an HttpOnly Cookie — ideally with the Secure attribute set for good measure — is preferable and will generally reduce the risk of Personally Identifiable Information (PII) leakage…but can only be used by a regular Web Application or when a BFF pattern is also employed.
  • For an Access Token, Cookie storage using HttpOnly — again, with the Secure attribute set for good measure — is a reasonable choice, at least for Access Tokens with a longer lifetime. However, again, this is only applicable for a regular Web Application or for a SPA where the BFF pattern is also employed. For security-sensitive operations, short-lived in-memory storage of an Access Token is typically preferable, as discussed above.
  • For a Refresh Token, that needs to be stored in a public client Browser context, an HttpOnly Cookie (ideally with the Secure attribute set for good measure) should be the preferred option. Ideally, and for the reasons previously discussed, storing a Refresh Token in a browser context should be avoided. However, where such a case is a necessity — as in a SPA without a BFF — a single-use, sometimes referred to as a Rotating Refresh Token, should also be used.

HttpOnly

Ordinarily, a Cookie can be manipulated using JavaScript, so a malicious script could potentially read or even modify information. However, a cookie with the HttpOnly attribute will be prevented by the Browser from being accessed in a JavaScript context, for example, using Document.cookie; when using HttpOnly, a Cookie can only be accessed by the backend server. Cookies that persist security-sensitive information, such as tokens, should always have the HttpOnly attribute set. This helps mitigate Cross-Site Scripting attacks (XSS), as previously discussed.

Secure Storage

Typically, a platform native storage mechanism associated with a native application (i.e. a Mobile App or Desktop Application), secure storage — often referred to as Keychain/Keystore storage, particularly in the case of (cross-platform) distribution — utilises specialised device hardware where data is stored cryptographically and in a tamper-proof manner. Using technologies that go by the name of Trusted Platform Module (Windows), Secure Enclave (Apple) and the like, this approach offers extremely secure token storage for confidential clients and a viable/often preferred alternative for public client devices, too:

  • For an ID Token, the use of secure storage prevents token misuse, particularly when it comes to the potential leakage of PII in cases where the token has to be stored for the duration (see the previous discussion above for more details). Of course, in an ideal scenario an ID Token would be used to establish the application session and then simply discarded to prevent misuse.
  • For an Access Token, the use of secure storage again prevents token misuse, and is a preferable choice for native clients where a Browser may not be utilised at all. Again, security-sensitive operations should prefer to leverage short-lived access tokens that are discarded after an ideally single use.
  • For a Refresh Token, secure storage is the recommended choice for native applications in a public client context. Single-use (sometimes referred to as Rotating) Refresh should also be preferred, but as previously discussed, this can introduce challenges from a multi-threading perspective — something inherently more prevalent in a native application context, and beyond the scope of this article.

Embedded Browsing

As described here in the OAuth 2.0 specification, native applications should prefer to use external browsers instead of embedding browsers when it comes to security-sensitive operations, in order to mitigate against Clickjacking attacks. However, techniques involving the use of postMessage to an iframe or the like (beyond the scope of this article) can be used to transfer an Access Token to a Browser context if caution is exercised. Never transfer a Refresh Token from a native context to a Browser context, embedded or otherwise, and avoid storage of a browser-transferred token where possible to mitigate XSS attacks.

Database Storage

For a backend implementation — such as a Web Server backend or a BFF — a database offers a mechanism for long-term storage across multiple execution instances. Database storage is implemented in a confidential client context, is queriable, and can be organised in a more complex manner than simple key-value pairs. As a token storage mechanism, the use of a database presents an attractive proposition for long-term persistence. However, the value of using a database as a token storage medium vs. the practicality, particularly from a security standpoint, is arguably questionable:

  • For an ID Token, the use of database storage doesn’t really hold much value. In an ideal scenario, an ID Token would be used to establish the application session and then be discarded to prevent misuse; even if the ID Token was required for the duration, an HttpOnly Cookie (as described here) would likely be a more convenient alternative.
  • For an Access Token, caching via the use of database storage in machine-to-machine scenarios — as in the use of Client Credentials grant to allocate an Access Token — could potentially mitigate excessive costs in situations where a 3rd-party SaaS CIAM solution is used. Solutions such as Auth0, for example, charge considerably more for machine-to-machine tokens, but don’t provide restrictions when it comes to machine-to-machine token allocation. As there are security considerations to bear in mind (see below) — and for other scenarios, as in the non-machine-to-machine use cases, the alternatives discussed in the previous sections are preferable — attempting Access Token caching is not advised without judicious security review. Further discussion on this is also beyond the scope of this article.
  • For a Refresh Token, on the face of it, database storage is the most resilient option, at least from a user experience perspective. For instance, using database storage can protect against any Authorization Server session expiry precipitating interactive user (re)authentication to call an API. It can also mitigate scenarios where Cookies are cleared, again forcing the user to do the same — i.e. interactively (re)authenticate. However, given the security considerations to be borne in mind (below), interactive (re)authentication is often the most preferable option and one that the user is comfortable with, particularly if the experience is well-designed.
    • The use of database storage offers some interesting possibilities when it comes to caching for agent-style operations performed on behalf of a user. OAuth 2.0 Token exchange, for instance, is a complicated feature, which few SaaS CIAM solutions support (so-called Native Token Exchange is a somewhat different feature that many Authorization Server implementations provide, but satisfies a different use case). So, for long-running API requests, which can be found in API Gateway scenarios and the like, Refresh Tokens cached using database storage can provide an alternative solution to the token expiration challenge. Given thesecurity considerations to bear in mind however (below), attempting database Refresh Token caching is likewise not advised without a judicious security review. Again, further discussion is also beyond the scope of this article.

With any database integration, the potential for attack, via SQL Injection or similar, is always a possibility; where security tokens are concerned — particularly Refresh Tokens — the fallout from a successful attack could be potentially catastrophic! Given such consideration, in the majority of cases, token storage using a database is not generally advised for reasons of security. However, in cases where it is attempted, the preference should be to use a NoSQL database — like Redis, for example — which is less susceptible to injection attacks. Of course, there are other security implications to look at where database storage of tokens is being considered, but those are beyond the scope of this article.

Client Side Database

The W3C Index Database API (3.0) defines a specification for a database of records holding simple values and hierarchical objects; each consisting of a key-value pair, and where an (optional) query syntax can be layered on top. This specification is designed to address the case where user agents — i.e. public client Browsers — need to store large numbers of objects locally in order to satisfy off-line data requirements of Web applications. The term Webstorage, as it’s sometimes referred to, and also synonymous with HTML5, is a client-side database that is useful for many things, but not normally recommended for use as a token storage vehicle.

Storage At A Glance

I’ve attempted to summarise available token storage options based on application type and the recommended best practices. As I said at the beginning, SaaS solutions typically incorporate the likes of SPAs, regular Web Applications, Mobile Apps, and Backend services, too, so there are typically multiple scenarios, in multiple different contexts, in which token storage needs to be considered. Having discussed the various pros and cons in the preceding narrative, let’s take a bird’s-eye view of the best practice recommendations in a nutshell (you’ll probably want to rotate your device if viewing on a mobile 😁).

Application TypeID TokenAccess TokenRefresh Token
SPAIn-Memory preferably;
HttpOnly + Secure Cookie using a BFF, or
Local Storage if persistence is required
In-Memory for security-sensitive operations;
HttpOnly + Secure Cookie using a BFF, or
Local Storage otherwise
HttpOnly + Secure Cookie utilising a BFF;
Local Storage otherwise*
Regular Web ApplicationIn-Memory preferably;
HttpOnly + Secure Cookie if persistence is required
In-Memory for security-sensitive operations;
HttpOnly + Secure Cookie otherwise
HttpOnly + Secure Cookie*
Native (i.e. Mobile or Desktop)In-Memory preferably;
Secure Storage if persistence is required
In-Memory for security-sensitive operations;
Secure Storage otherwise
Secure Storage*

* = Using single-use/rotation where possible

Advanced Security Measures

Whilst not specifically the province of this article, I felt it was worth mentioning a few of the additional security measures some of the more advanced SaaS CIAM solutions provide — Keycloak, being one in particular that springs to mind — which also have a bearing on tokens that may have been maliciously exfiltrated from storage. I won’t go into too much detail, but maybe I will come back to revisit some of these in future articles 😎:

  • Token Binding: where a token is bound for use with a specific client or TLS session, thus preventing stolen tokens from being used elsewhere.
  • Device Identification: where device IDs are stored against tokens to bind sessions to specific devices, helping anomaly detection and also facilitating conditional access.
  • Anomaly Detection: to monitor for unusual token use (e.g., from different geographies or in rapid succession).
  • Consent and Scope Enforcement: to ensure token scopes are minimised (think Principle of Least Privilege).
  • Secure Logout: to ensure tokens are revoked on logout, storage is cleared, and sessions are ended at the IdP level to avoid silent (re)authentication.

Got questions?
Feel free to reach out!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *