{"id":209,"date":"2025-07-06T14:11:36","date_gmt":"2025-07-06T13:11:36","guid":{"rendered":"https:\/\/discovery.cevolution.co.uk\/ciam\/?p=209"},"modified":"2025-10-28T12:13:28","modified_gmt":"2025-10-28T12:13:28","slug":"spotlight-on-the-ciam-token-storage-conundrum","status":"publish","type":"post","link":"https:\/\/discovery.cevolution.co.uk\/ciam\/2025\/07\/06\/spotlight-on-the-ciam-token-storage-conundrum\/","title":{"rendered":"Spotlight on the CIAM Token Storage Conundrum"},"content":{"rendered":"<span class=\"span-reading-time rt-reading-time\" style=\"display: block;\"><span class=\"rt-label rt-prefix\">Reading Time: <\/span> <span class=\"rt-time\"> 16<\/span> <span class=\"rt-label rt-postfix\">minutes<\/span><\/span>\n<p>From a CIAM perspective, a <span class=\"popup-trigger popmake-1354\" data-popup-id=\"1354\" data-do-default=\"0\">B2C<\/span>\/<span class=\"popup-trigger popmake-418\" data-popup-id=\"418\" data-do-default=\"0\">B2B<\/span> 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 <span class=\"popup-trigger popmake-1393\" data-popup-id=\"1393\" data-do-default=\"0\"><strong>ID Token<\/strong><\/span>, for instance \u2014 used by an application to identify an authenticated user and build the user session \u2014 is expressed as a <span class=\"popup-trigger popmake-1899\" data-popup-id=\"1899\" data-do-default=\"0\">JWT<\/span>. <\/p>\n\n\n\n<p>An <span class=\"popup-trigger popmake-1400\" data-popup-id=\"1400\" data-do-default=\"0\"><strong>Access Token<\/strong><\/span>, on the other hand \u2014 used when calling an API on behalf of a user \u2014 is more often than not expressed as a JWT, but could equally be an opaque artefact requiring the use of <em>Token Introspection<\/em>. Whilst a <strong><span class=\"popup-trigger popmake-4495\" data-popup-id=\"4495\" data-do-default=\"0\">Refresh Token<\/span><\/strong> \u2014 used primarily to recreate an expired Access Token without user interaction \u2014 is typically opaque.<\/p>\n\n\n\n<p>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 <span class=\"popup-trigger popmake-1754\" data-popup-id=\"1754\" data-do-default=\"0\">malicious actors<\/span>; how and where they are stored, then, becomes a crucial aspect of a secure CIAM implementation. <\/p>\n\n\n\n<p>Earlier this year, I came across the following post on X\/Twitter (whichever your preference \ud83d\ude09) 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?<\/p>\n\n\n\n<div class=\"wp-block-group is-content-justification-center is-nowrap is-layout-flex wp-container-core-group-is-layout-23441af8 wp-block-group-is-layout-flex\">\n<figure class=\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\"><div class=\"wp-block-embed__wrapper\">\n<div class=\"embed-twitter\"><blockquote class=\"twitter-tweet\" data-width=\"500\" data-dnt=\"true\"><p lang=\"en\" dir=\"ltr\">Yesterday, a customer insisted that Auth0 recommends localstorage for long-lived session tokens in React<br><br>I could have sworn they strongly advise against it \ud83e\udd14<br><br>Wayback Machine shows they changed their guidance, and removed the link to OWASP that says not use localstorage \ud83d\udc40 <a href=\"https:\/\/t.co\/1FPZ4JDp0L\">pic.twitter.com\/1FPZ4JDp0L<\/a><\/p>&mdash; Colin | clerk.com (@tweetsbycolin) <a href=\"https:\/\/twitter.com\/tweetsbycolin\/status\/1868044046381568275?ref_src=twsrc%5Etfw\">December 14, 2024<\/a><\/blockquote><script async src=\"https:\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"><\/script><\/div>\n<\/div><\/figure>\n<\/div>\n\n\n\n<p>Whilst <a href=\"https:\/\/x.com\/tweetsbycolin\" target=\"_blank\" rel=\"noopener\" title=\"\">Colin<\/a> was talking specifically about browser-based scenarios \u2014 which are arguably the most challenging \u2014 SaaS solutions typically incorporate the likes of <span class=\"popup-trigger popmake-2862\" data-popup-id=\"2862\" data-do-default=\"0\">Mobile Apps<\/span>, as well as <span class=\"popup-trigger popmake-2870\" data-popup-id=\"2870\" data-do-default=\"0\">Backend<\/span> services too (whether as part of a regular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span>, or as a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> in some <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> context or the like). So there are typically multiple scenarios, in multiple different contexts, in which token storage needs to be considered. <\/p>\n\n\n\n<p>My name&#8217;s <span class=\"popup-trigger popmake-378\" data-popup-id=\"378\" data-do-default=\"0\">Peter Fernandez<\/span>, and in this article, I&#8217;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 <em>Token Storage Conundrum<\/em>.<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"storage-at-a-glance\">Storage At A Glance<\/h2>\n\n\n<p>In true TL;DR fashion, I&#8217;ve attempted to summarise available token storage options based on application type and the recommended best practices. <span class=\"popup-trigger popmake-2946\" data-popup-id=\"2946\" data-do-default=\"0\">SaaS<\/span> solutions typically incorporate the likes of <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPAs<\/span>, regular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Applications<\/span>, <span class=\"popup-trigger popmake-2862\" data-popup-id=\"2862\" data-do-default=\"0\">Mobile Apps<\/span>, and <span class=\"popup-trigger popmake-2870\" data-popup-id=\"2870\" data-do-default=\"0\">Backend<\/span> services, too, so there are typically multiple scenarios, in multiple different contexts, in which token storage needs to be considered. <\/p>\n\n\n\n<p>I&#8217;ll be discussing the various pros and cons in more detail in the rest of the narrative in this article, but for now, let&#8217;s take a bird&#8217;s-eye view of the best practice recommendations in a nutshell; you&#8217;ll probably want to rotate your device (to a landscape orientation) if viewing on a mobile device \ud83d\ude01:<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-f9c50089569744e02a35311a8edf83b2 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>A <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> will typically enable you to reduce running costs as \u2014 outside of the initial hydration, or other <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> specific workflow \u2014 all of the application logic runs in the front end. Web server costs are typically kept to a minimum, so you will likely want to try not to compromise that when integrating a CIAM solution. <\/em>Third-party SaaS CIAM vendor <a href=\"https:\/\/curity.io\" target=\"_blank\" rel=\"noopener\" title=\"\">Curity<\/a> provides a really good <a href=\"https:\/\/curity.io\/resources\/learn\/spa-best-practices\/\" title=\"\">SPA Best Practices page,<\/a> which is also worth a read.<\/p>\n<\/div>\n\n\n\n<figure class=\"wp-block-table has-medium-font-size\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-center\" data-align=\"center\">Application Type<\/th><th class=\"has-text-align-center\" data-align=\"center\">ID Token<\/th><th class=\"has-text-align-center\" data-align=\"center\">Access Token<\/th><th class=\"has-text-align-center\" data-align=\"center\">Refresh Token<\/th><\/tr><\/thead><tbody><tr><td class=\"has-text-align-center\" data-align=\"center\"><span class=\"popup-trigger popmake-2828 \" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span><\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> preferably;<br><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong> (using a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span>), or <br><strong><a href=\"#local-storage\" title=\"\">Local Storage<\/a><\/strong> if persistence is required<\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> for security-sensitive operations;<br><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong> (using a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span>), or <br><strong><a href=\"#local-storage\" title=\"\">Local Storage<\/a><\/strong> otherwise<\/td><td class=\"has-text-align-center\" data-align=\"center\"><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong> (using a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span>); <br><strong><a href=\"#local-storage\" title=\"\">Local Storage<\/a><\/strong> otherwise*<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\"><span class=\"popup-trigger popmake-2852 \" data-popup-id=\"2852\" data-do-default=\"0\">Regular Web Application<\/span><\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> preferably;<br><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong> if persistence is required<\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> for security-sensitive operations;<br><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong> otherwise<\/td><td class=\"has-text-align-center\" data-align=\"center\"><code>HttpOnly<\/code> + <code>Secure<\/code> <strong><a href=\"#cookies\" title=\"\">Cookie<\/a><\/strong>*<\/td><\/tr><tr><td class=\"has-text-align-center\" data-align=\"center\">Native (i.e. <span class=\"popup-trigger popmake-2862\" data-popup-id=\"2862\" data-do-default=\"0\">Mobile<\/span> or <span class=\"popup-trigger popmake-2866\" data-popup-id=\"2866\" data-do-default=\"0\">Desktop<\/span>)<\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> preferably;<br><strong><a href=\"#secure-storage\" title=\"\">Secure Storage<\/a><\/strong> if persistence is required<\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#in-memory\" title=\"\">In-Memory<\/a><\/strong> for security-sensitive operations;<br><strong><a href=\"#secure-storage\" title=\"\">Secure Storage<\/a><\/strong> otherwise<\/td><td class=\"has-text-align-center\" data-align=\"center\"><strong><a href=\"#secure-storage\" title=\"\">Secure Storage<\/a><\/strong>*<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>* = Using single-use\/rotation where possible<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"in-memory\">In-Memory Storage<\/h2>\n\n\n<p>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 <em>object<\/em> 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 &#8220;stored&#8221;: 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 <span class=\"popup-trigger popmake-2870\" data-popup-id=\"2870\" data-do-default=\"0\">Backend<\/span>, until a request is complete.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-c8308b81eb550853501747c8dc19b44f is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Backends often utilise connection pooling, where an execution context may persist longer than anticipated to service other incoming requests \u2014 the upside being that context setup and teardown are mitigated during periods of heavy traffic. However, Backends are what are commonly referred to as <span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">Confidential Clients<\/span>, so inherently more secure. The lifetime of an execution context is also rarely predictable. <\/em><\/p>\n<\/div>\n\n\n\n<p>Returning to the &#8220;tweet&#8221; Colin made above, in their <a href=\"https:\/\/auth0.com\/docs\/secure\/security-guidance\/data-security\/token-storage#browser-in-memory-scenarios\" target=\"_blank\" rel=\"noreferrer noopener\">latest documentation<\/a> \u2014 at least at the time of writing this article \u2014 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 <a href=\"#web-workers\">below<\/a> 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 \u2014 a good thing from a security perspective, but also potentially problematic:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li id=\"inmemoryidtoken\"><strong>For an ID Token<\/strong>, this isn&#8217;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 <span class=\"popup-trigger popmake-415\" data-popup-id=\"415\" data-do-default=\"0\">IdP<\/span> of choice, and also your preferred mechanism for Logout \u2014 i.e. Logout from the IdP as well as the application \u2014 an ID Token, regardless of its expiration, may need to persist for the duration so that it can be passed via <code>id_token_hint<\/code> as recommended in the <a href=\"https:\/\/openid.net\/specs\/openid-connect-rpinitiated-1_0.html#RPLogout\" target=\"_blank\" rel=\"noopener\" title=\"\">OpenID spec<\/a>.<\/li>\n\n\n\n<li id=\"inmemoryaccesstoken\"><strong>For an Access Token<\/strong> that is used as part of a security-sensitive operation \u2014 such as transferring a sum of money or the like \u2014 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 <em>without<\/em> needing to make corresponding calls to the Authorization Server (i.e. to get a new token to call the API).<\/li>\n\n\n\n<li><strong>For a Refresh Token<\/strong>, 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.<\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-5ded8c56fadf7bdb085cc5626ae34f37 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>One caveat is that use of an in-memory persistent cache \u2014 such as <a href=\"https:\/\/redis.io\" target=\"_blank\" rel=\"noopener\" title=\"\">Redis<\/a> or <code>memcached<\/code> \u2014 offers a good solution for backend confidential clients that is often preferable to using a database (discussed <a href=\"#database\" title=\"\">below<\/a>) when it comes to Refresh Token storage. <\/em><\/p>\n<\/div>\n\n\n\n<p>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 &#8220;security by obscurity&#8221;: there&#8217;s nothing typically stopping malicious access, it&#8217;s just that if malicious code doesn&#8217;t know where something is, it can&#8217;t find it. In the case of a JavaScript <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span>, for example, a script isn&#8217;t necessarily prevented from accessing a token held in memory; rather, it&#8217;s the fact that a symbolic reference to the token is not well-known which makes it harder for a malicious script to find.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-e5ff79573982bae615f667fe2feb3820 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>By treating in-memory storage as a security-sensitive resource \u2014 as in explicitly scrubbing (i.e. overwriting) token memory once a token is no longer required \u2014 the surface for attack can be further reduced. Holding a token in memory for no longer than is required should always be preferred.<\/em><\/p>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"web-workers\">Web Workers<\/h3>\n\n\n<p>A Web Worker (see the Mozilla documentation <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Workers_API\/Using_web_workers\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> 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 <code>window<\/code> is unavailable for reference. Thus making it even harder for a malicious script to hunt down a token via some symbolic reference mechanism. <\/p>\n\n\n\n<p>Dedicated workers, represented by a&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/DedicatedWorkerGlobalScope\"><code>DedicatedWorkerGlobalScope<\/code><\/a>&nbsp;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&nbsp;<code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/SharedWorkerGlobalScope\">SharedWorkerGlobalScope<\/a><\/code>, provides storage that can be accessed from multiple scripts. <\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-9a8919c39168882b0f3a77b044cf129c is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>The <strong>OWASP HTML 5 Security Cheat Sheet<\/strong> <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/HTML5_Security_Cheat_Sheet.html#web-workers\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> has additional information on the use of Web Workers, including some additional things to consider.<\/em><\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"local-storage\">Local Storage<\/h2>\n\n\n<p>Typically associated with Browser execution <em>\u2014<\/em> i.e. embedded browser execution as well as execution via an externalised chrome<em> <em>\u2014<\/em><\/em> <code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/localStorage\" target=\"_blank\" rel=\"noopener\" title=\"\">localStorage<\/a><\/code> allows data to be stored in <em>key\/value pair<\/em> fashion on a <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span>, 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 <strong>no automatic expiration<\/strong>. <\/p>\n\n\n\n<p>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 <strong>OWASP HTML 5 Security Cheat Sheet<\/strong> <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/HTML5_Security_Cheat_Sheet.html#local-storage\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a>, the recommendation is that local storage be avoided where sensitive information is concerned.<\/p>\n\n\n\n<p id=\"xss\">Local Storage is particularly susceptible to what is commonly referred to as Cross-Site Scripting attacks (or XSS for short; see the <strong><a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\" title=\"\">OWASP Cross Site Scripting Prevention Cheat Sheet<\/a><\/strong> 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.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-b585bbecc23a4377e03b52e9b452daab is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Local storage is most vulnerable to XSS attacks, as there is no way to protect against a malicious script reading or even modifying its content. XSS attacks are often convoluted and complex, and how they work is beyond the scope of this article.<\/em><\/p>\n<\/div>\n\n\n\n<p>As local storage is arguably the most vulnerable, OWASP advises that any data stored should also not be trusted. Unlike when using the <code>Path<\/code> attribute of an HTTP <em>Cookie<\/em> (discussed <a href=\"#cookies\" title=\"\">below<\/a>), 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 <em><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Security\/Same-origin_policy\" target=\"_blank\" rel=\"noopener\" title=\"\">Same Origin Policy<\/a><\/em> rather than any specific path restriction. So the recommendation is also to avoid hosting unrelated applications that use local storage on the same origin&#8230;as all of them would share the same <code>localStorage<\/code>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li id=\"localstorageidtoken\"><strong>For an ID Token<\/strong>, using local storage means that Personally Identifiable Information (<span class=\"popup-trigger popmake-2915\" data-popup-id=\"2915\" data-do-default=\"0\">PII<\/span>) could potentially be leaked, particularly where <code>email<\/code> and\/or <code>profile<\/code> were included as a scope when an authentication request was sent to the <span class=\"popup-trigger popmake-415\" data-popup-id=\"415\" data-do-default=\"0\">IdP<\/span> (see the OpenID spec <a href=\"https:\/\/openid.net\/specs\/openid-connect-core-1_0.html#ScopeClaims\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> 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.\n<ul class=\"wp-block-list\">\n<li id=\"localstorageidtoken\">Ideally, a Cookie using the <code>httpOnly<\/code> flag, as discussed <a href=\"#cookies\" title=\"\">below<\/a>, 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 <a href=\"#inmemoryidtoken\" title=\"\">above<\/a>). However, in a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> (Single Page Application), the choice between using a Cookie or local storage is arguably an arbitrary one, unless a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> \u2014 Backend For Front-end \u2014 pattern is also being employed. <\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>For an Access Token<\/strong>, 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 <code><a href=\"https:\/\/openid.net\/specs\/openid-connect-core-1_0.html#UserInfo\" target=\"_blank\" rel=\"noopener\" title=\"\">userinfo<\/a><\/code> endpoint, even when not in <span class=\"popup-trigger popmake-1899\" data-popup-id=\"1899\" data-do-default=\"0\">JWT<\/span> format, an (opaque) Access Token can still potentially leak PII!\n<ul class=\"wp-block-list\">\n<li>Again, a Cookie using the <code>httpOnly<\/code> flag (discussed <a href=\"#cookies\" title=\"\">below<\/a>) would be the preferred choice for storing a longer-lived Access Token. However, for a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span>, the choice between using a Cookie or local storage is again an arbitrary one, unless a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> pattern is also being employed. As <a href=\"#inmemoryaccesstoken\" title=\"\">previously discussed<\/a>, in-memory should still be the preferred choice where Access Tokens for security-sensitive operations are concerned.  <\/li>\n<\/ul>\n<\/li>\n\n\n\n<li id=\"localstoragerefreshtoken\"><strong>For a Refresh Token<\/strong>, 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.\n<ul class=\"wp-block-list\">\n<li>Some 3rd party SaaS CIAM solutions, such as Auth0, recommend using Refresh Tokens in a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> 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 <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> \u2014 as in one where a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> is not being used \u2014 then local storage is your only option.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-a09e843dd6b1eaf0e8af15bf574e08f3 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Where local storage of a Refresh Token is necessary, a single-use (sometimes referred to as Rotating) Refresh Token should ideally be used. Single-use refresh tokens come with their own challenges \u2014 particularly in respect to multi-threading \u2014 however, that is a topic beyond the scope of this article. <\/em> <em> <\/em><\/p>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"session-storage\">Session Storage<\/h3>\n\n\n<p>Whilst similar in its implementation, the difference here is that while local storage is partitioned by origin only \u2014 i.e. utilising <em><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/Security\/Same-origin_policy\" target=\"_blank\" rel=\"noopener\" title=\"\">Same Origin Policy<\/a><\/em> \u2014 <code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Window\/sessionStorage\" target=\"_blank\" rel=\"noopener\" title=\"\">sessionStorage<\/a><\/code> 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&#8217;s associated remains open. Again, whilst there is no automatic expiry per se, at least the data contained in session storage doesn&#8217;t stay around forever. Session storage is also isolated, so in the case of the browser, each tab has its own separate <code>sessionStorage<\/code> block.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-f0b7b85b8c9fa0ca8683b5c2a6b6d542 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Session storage is still vulnerable to XSS attacks, though the attack surface is arguably reduced given that persistence and visibility are more tightly constrained.<\/em><\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"cookies\">Cookie Storage<\/h2>\n\n\n<p>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 <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Guides\/Cookies\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> 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:<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-a6993302ed6fa8fb89a802e74f3e78f3 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>When using cookies, clearing the Browser cookie cache will typically force user (re)authentication \u2014 in an interactive manner too, if the session with the <span class=\"popup-trigger popmake-415 \" data-popup-id=\"415\" data-do-default=\"0\">IdP<\/span> has also expired.<\/em><\/p>\n<\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li>a Cookie is more often associated with a regular <span class=\"popup-trigger popmake-2852 \" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span> than with a <span class=\"popup-trigger popmake-2828 \" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span>. <\/li>\n\n\n\n<li>a Cookie is included as part of HTTP request\/response communication\n<ul class=\"wp-block-list\">\n<li>typically as part of the HTTP Header. <\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>a Cookie can have an associated <code>Expires<\/code>, or <code>Max-Age<\/code>, so that explicit expiry occurs in an automated fashion<\/li>\n\n\n\n<li>a&nbsp;Cookie <code>Path<\/code>&nbsp;attribute indicates a URL path that must exist in the requested URL in order to send the&nbsp;Cookie.\n<ul class=\"wp-block-list\">\n<li>the optional <code>Domain<\/code> attribute can also be used to specify on which server the Cookie can be received.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>a Cookie with the <code>Secure<\/code> attribute will only be sent to the server with an encrypted request over the <span class=\"popup-trigger popmake-3397 \" data-popup-id=\"3397\" data-do-default=\"0\">HTTPS<\/span> protocol.<\/li>\n\n\n\n<li>a Cookie with the&nbsp;<code>HttpOnly<\/code>&nbsp;attribute can&#8217;t be accessed via JavaScript; more on this <a href=\"#httponly\" title=\"\">below<\/a>.&nbsp; <\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-6dfabd3795c110e87b64778e7002bb73 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>A Cookie without the <code>HttpOnly<\/code> attribute is equally vulnerable to XSS attacks as using Local Storage (see the <a href=\"#xss\" title=\"\">previous section<\/a> for more details). So, for a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span>, consider also implementing the <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> pattern if possible in order to provide the most secure option.<\/em><\/p>\n<\/div>\n\n\n\n<p>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 <code>HttpOnly<\/code> attribute is used; discussed in more detail <a href=\"#httponly\" title=\"\">below<\/a>. For a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> (Single Page Application), the preferred <code>HttpOnly<\/code> attribute can&#8217;t be used unless a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> \u2014 Backend For Front-end \u2014 pattern is also employed. So, for a vanilla SPA (as in one that doesn&#8217;t employ a BFF), there is arguably little difference between using a Cookie vs using local storage.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-e92e67edff9198ae221835dbcb5e8417 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>For a vanilla SPA, local storage could be considered marginally more secure (and more efficient, too), as data is not included as part of HTTP communication. For a regular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span>, an <code>HttpOnly<\/code> Cookie, however, would be the preferred choice.<em> <\/em><\/em> <em> <\/em><\/p>\n<\/div>\n\n\n\n<p>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). <\/p>\n\n\n\n<p>The additional attributes that can be used to enforce control over a Cookie \u2014 i.e. <code>Max-Age<\/code> (<code>Expires<\/code>), <code>Path<\/code>, <code>Secure<\/code>, and <code>HttpOnly<\/code> in particular \u2014 arguably make it a generally safer option when it comes to token storage, but only if they are\/can be used:<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-d466bca5037becd553460e6b103068ca is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>The maximum amount of data a Cookie contains is limited; Cookies are transmitted as part of each HTTP request\/response, so keeping the number of Cookies to a minimum, as well as the amount of data each Cookie contains, improves overall efficiency from a network perspective. The amount of data contained in a JWT has an impact on whether or not it can be stored in a Cookie.<em> <\/em><\/em> <em> <\/em><\/p>\n<\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li id=\"cookieidtoken\"><strong>For an ID Token<\/strong>, 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 <a href=\"#localstorageidtoken\" title=\"\">previous section<\/a>), using an <code>HttpOnly<\/code> Cookie \u2014 ideally with the <code>Secure<\/code> attribute set for good measure \u2014 is preferable and will generally reduce the risk of Personally Identifiable Information (<span class=\"popup-trigger popmake-2915\" data-popup-id=\"2915\" data-do-default=\"0\">PII<\/span>) leakage&#8230;but can only be used by a regular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span> or when a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> pattern is also employed.<\/li>\n\n\n\n<li><strong>For an Access Token<\/strong>, Cookie storage using <code>HttpOnly<\/code> \u2014 again, with the <code>Secure<\/code> attribute set for good measure \u2014 is a reasonable choice, at least for Access Tokens with a longer lifetime. However, again, this is only applicable for a regular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span> or for a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> where the <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> pattern is also employed. For security-sensitive operations, short-lived in-memory storage of an Access Token is typically preferable, as <a href=\"#inmemoryaccesstoken\" title=\"\">discussed above<\/a>. <\/li>\n\n\n\n<li><strong>For a Refresh Token<\/strong>, that needs to be stored in a <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span> Browser context, an <code>HttpOnly<\/code> Cookie (ideally with the <code>Secure<\/code> attribute set for good measure) should be the preferred option. Ideally, and for the reasons <a href=\"#localstoragerefreshtoken\" title=\"\">previously discussed<\/a>, storing a Refresh Token in a browser context should be avoided. However, where such a case is a necessity \u2014 as in a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> without a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> \u2014 a single-use, sometimes referred to as a Rotating Refresh Token, should also be used.<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\" id=\"httponly\"><code>HttpOnly<\/code><\/h3>\n\n\n<p>Ordinarily, a Cookie can be manipulated using JavaScript, so a malicious script could potentially read or even modify information. However, a cookie with the&nbsp;<code>HttpOnly<\/code>&nbsp;attribute will be prevented by the Browser from being accessed in a JavaScript context, for example, using&nbsp;<code><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Document\/cookie\">Document.cookie<\/a><\/code>; when using <code>HttpOnly<\/code>, a Cookie can only be accessed by the backend server. Cookies that persist security-sensitive information, such as tokens, should always have the&nbsp;<code>HttpOnly<\/code>&nbsp;attribute set, which helps mitigate Cross-Site Scripting attacks (XSS), as previously discussed.<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-00b1dc2999c90c14f74214fbe61bd3e5 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>As discussed, use of the  <code>HttpOnly<\/code> attribute is only pertinent for a <\/em>r<em>egular <span class=\"popup-trigger popmake-2852\" data-popup-id=\"2852\" data-do-default=\"0\">Web Application<\/span> or for a <span class=\"popup-trigger popmake-2828\" data-popup-id=\"2828\" data-do-default=\"0\">SPA<\/span> where the <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF <\/span>pattern is also being used. The use of the <code>HttpOnly<\/code> attribute does not completely eliminate the possibility of XSS, but it does significantly reduce it (particularly when also combined with the use of the <code>Path<\/code> attribute). <\/em> <em> <\/em><\/p>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"secure-storage\">Secure Storage<\/h2>\n\n\n<p>Typically, a platform native storage mechanism associated with a native application (i.e. a <span class=\"popup-trigger popmake-2862\" data-popup-id=\"2862\" data-do-default=\"0\">Mobile App<\/span> or <span class=\"popup-trigger popmake-2866\" data-popup-id=\"2866\" data-do-default=\"0\">Desktop Application<\/span>), secure storage \u2014 often referred to as Keychain\/Keystore storage, particularly in the case of (cross-platform) distribution \u2014 utilises specialised device hardware where data is stored cryptographically and in a tamper-proof manner. <\/p>\n\n\n\n<p>Using technologies that go by the name of Trusted Platform Module (<a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/security\/hardware-security\/tpm\/trusted-platform-module-overview\" target=\"_blank\" rel=\"noopener\" title=\"\">Windows<\/a>), Secure Enclave (<a href=\"https:\/\/support.apple.com\/en-gb\/guide\/security\/sec59b0b31ff\/web\" target=\"_blank\" rel=\"noopener\" title=\"\">Apple<\/a>) and the like, this approach offers <strong>extremely<\/strong> secure token storage for <span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">confidential clients<\/span> and a viable\/often preferred alternative for <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span> devices, too:<\/p>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-1cdd39f2cfef6feece0948f4b9a1d83a is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>Secure Storage can be utilised as the underlying storage mechanism for Browser in-memory or Browser local storage; however, in such cases, the use of secure storage is completely under Browser control and hidden from the application.<\/em> <em> <\/em><\/p>\n<\/div>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>For an ID Token<\/strong>, the use of secure storage prevents token misuse, particularly when it comes to the potential leakage of <span class=\"popup-trigger popmake-2915\" data-popup-id=\"2915\" data-do-default=\"0\">PII<\/span> in cases where the token has to be stored for the duration (see the previous discussion <a href=\"#inmemoryidtoken\" title=\"\">above<\/a> 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.<\/li>\n\n\n\n<li><strong>For an Access Token<\/strong>, 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.<\/li>\n\n\n\n<li><strong>For a Refresh Token<\/strong>, secure storage is the recommended choice for native applications in a <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span> 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 \u2014 something inherently more prevalent in a native application context, and beyond the scope of this article.<\/li>\n<\/ul>\n\n\n<h3 class=\"wp-block-heading\" id=\"distribution\">Embedded Browsing<\/h3>\n\n\n<p>As described <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc6749#section-10.13\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a> 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 <a href=\"https:\/\/owasp.org\/www-community\/attacks\/Clickjacking\" target=\"_blank\" rel=\"noopener\" title=\"\">Clickjacking<\/a> attacks. However, techniques involving the use of <code>postMessage<\/code> to an <code>iframe<\/code> 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. <strong>Never<\/strong> 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.<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"database\">Database Storage<\/h2>\n\n\n<p>For a backend implementation \u2014 such as a <span class=\"popup-trigger popmake-893\" data-popup-id=\"893\" data-do-default=\"0\">Web Server<\/span> backend or a <span class=\"popup-trigger popmake-2873\" data-popup-id=\"2873\" data-do-default=\"0\">BFF<\/span> \u2014 a database offers a mechanism for long-term storage across multiple execution instances. Database storage is implemented in a <span class=\"popup-trigger popmake-578\" data-popup-id=\"578\" data-do-default=\"0\">confidential client<\/span> 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:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>For an ID Token<\/strong>, the use of database storage doesn&#8217;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 <code>HttpOnly<\/code> Cookie (as described <a href=\"#cookieidtoken\" target=\"_blank\" rel=\"noopener\" title=\"\">here<\/a>) would likely be a more convenient alternative.<\/li>\n\n\n\n<li><strong>For an Access Token<\/strong>, caching via the use of database storage in machine-to-machine scenarios \u2014 as in the use of <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6749#section-4.4\" target=\"_blank\" rel=\"noopener\" title=\"\">Client Credentials<\/a> grant to allocate an Access Token \u2014 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&#8217;t provide restrictions when it comes to machine-to-machine token allocation. As there are security considerations to bear in mind (see below) \u2014 and for other scenarios, as in the non-machine-to-machine use cases, the alternatives discussed in the previous sections are preferable \u2014 attempting Access Token caching is not advised without judicious security review. Further discussion on this is also beyond the scope of this article.<\/li>\n\n\n\n<li><strong>For a Refresh Token<\/strong>, 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 \u2014 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.\n<ul class=\"wp-block-list\">\n<li>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 <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc8693\" target=\"_blank\" rel=\"noopener\" title=\"\">Token exchange<\/a>, for instance, is a complicated feature, which few SaaS CIAM solutions support (so-called <em>Native Token Exchange<\/em> 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 the security considerations to bear in mind, however (see 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.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-group has-base-color has-accent-4-background-color has-text-color has-background has-link-color wp-elements-4fe8ef60934ce5c17ed19353e2a07d89 is-layout-flow wp-block-group-is-layout-flow\" style=\"border-radius:20px\">\n<p class=\"has-text-align-center\" style=\"padding-top:var(--wp--preset--spacing--40);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--40);padding-left:var(--wp--preset--spacing--40)\"><em>In its latest <a href=\"https:\/\/auth0.com\/ai\" target=\"_blank\" rel=\"noopener\" title=\"\">Auth for GenAI<\/a>, the Auth0 Token Vault is an example of where database storage is being utilised for the caching of Refresh Tokens used by AI agents. <\/em><\/p>\n<\/div>\n\n\n\n<p>With any database integration, the potential for attack, via <a href=\"https:\/\/owasp.org\/www-community\/attacks\/SQL_Injection\" target=\"_blank\" rel=\"noopener\" title=\"\">SQL Injection<\/a> or similar, is always a possibility; where security tokens are concerned \u2014 particularly Refresh Tokens \u2014 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 \u2014 like <a href=\"https:\/\/redis.io\" target=\"_blank\" rel=\"noopener\" title=\"\">Redis<\/a>, for example \u2014 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.<\/p>\n\n\n<h3 class=\"wp-block-heading\" id=\"client-side-database\">Client Side Database<\/h3>\n\n\n<p>The <a href=\"https:\/\/www.w3.org\/TR\/IndexedDB\/\" target=\"_blank\" rel=\"noopener\" title=\"\">W3C Index Database API (3.0)<\/a> 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 \u2014 i.e. <span class=\"popup-trigger popmake-582\" data-popup-id=\"582\" data-do-default=\"0\">public client<\/span> Browsers \u2014 need to store large numbers of objects locally in order to satisfy off-line data requirements of Web applications.&nbsp;The term <em>Webstorage,<\/em> as it&#8217;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.<\/p>\n\n\n<h2 class=\"wp-block-heading\" id=\"advanced-security-measures\">Advanced Security Measures<\/h2>\n\n\n<p>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 \u2014 <a href=\"https:\/\/www.keycloak.org\" target=\"_blank\" rel=\"noopener\" title=\"\">Keycloak<\/a>, being one in particular that springs to mind \u2014 which also have a bearing on tokens that may have been maliciously exfiltrated from storage. I won&#8217;t go into too much detail, but maybe I will come back to revisit some of these in future articles \ud83d\ude0e:  <\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Token Binding<\/strong>: where a token is bound for use with a specific client or TLS session, thus preventing stolen tokens from being used elsewhere.<\/li>\n\n\n\n<li><strong>Device Identification<\/strong>: where device IDs are stored against tokens to bind sessions to specific devices, helping anomaly detection and also facilitating conditional access.<\/li>\n\n\n\n<li><strong>Anomaly Detection<\/strong>: to monitor for unusual token use (e.g., from different geographies or in rapid succession).<\/li>\n\n\n\n<li><strong>Consent and Scope Enforcement<\/strong>: to ensure token scopes are minimised (think <em><span class=\"popup-trigger popmake-4375 \" data-popup-id=\"4375\" data-do-default=\"0\">Principle of Least Privilege<\/span><\/em>).<\/li>\n\n\n\n<li><strong>Secure Logout<\/strong>: to ensure tokens are revoked on logout, storage is cleared, and sessions are ended at the <span class=\"popup-trigger popmake-415\" data-popup-id=\"415\" data-do-default=\"0\">IdP<\/span> level to avoid silent (re)authentication.<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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. <\/p>\n","protected":false},"author":1,"featured_media":4512,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"authenticate":"","authentication":"","authenticatedMethod":"","authenticatedMember":"","authorizedPermissions":[],"_jetpack_memberships_contains_paid_content":false,"footnotes":"[]","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":false,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"enabled":false},"version":2},"_links_to":"","_links_to_target":""},"categories":[14,8],"tags":[22,20,18,82,81],"class_list":["post-209","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-authentication","category-authorization","tag-ciam","tag-oauth2","tag-oidc","tag-storage","tag-tokens"],"aioseo_notices":[],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"https:\/\/discovery-bucket-ha60ib.s3.eu-west-2.amazonaws.com\/wp-content\/uploads\/sites\/22\/2025\/07\/04173904\/create-a-featured-image-for-a-blog-post-titled-spotlight.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/209","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/comments?post=209"}],"version-history":[{"count":92,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/209\/revisions"}],"predecessor-version":[{"id":4984,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/posts\/209\/revisions\/4984"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/media\/4512"}],"wp:attachment":[{"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/media?parent=209"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/categories?post=209"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/discovery.cevolution.co.uk\/ciam\/wp-json\/wp\/v2\/tags?post=209"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}