Keywords: refresh token | Single-Page Application | OAuth 2.0 | PKCE | secure storage
Abstract: This article explores the secure storage of refresh tokens in Single-Page Applications (SPAs). By analyzing the limitations of traditional storage methods and integrating the latest security standards like OAuth 2.0 and PKCE, it proposes solutions based on in-memory storage and the Authorization Code with PKCE flow. The paper details how to mitigate XSS and CSRF attacks and emphasizes the importance of using existing authentication libraries.
In the authentication architecture of Single-Page Applications (SPAs), the secure storage of refresh tokens is a critical yet often overlooked issue. Traditional storage methods such as local storage or session storage pose security risks, as they are vulnerable to Cross-Site Scripting (XSS) attacks. While storing access tokens in HttpOnly cookies can mitigate XSS risks, handling refresh tokens in the same manner exposes them to Cross-Site Request Forgery (CSRF) attacks.
Limitations of Traditional Storage Methods
Many developers prefer to store access tokens in HttpOnly cookies to leverage their built-in XSS protection. However, if refresh tokens are also stored in cookies, they are sent with every request to the resource server, increasing the risk of CSRF attacks. Worse, if both tokens are stored in the same location, an attacker who breaches the defenses gains access to both, creating a dual security vulnerability.
Alternative: In-Memory Storage
A viable solution is to store refresh tokens in JavaScript memory variables. This method effectively isolates refresh tokens from regular requests, preventing them from being automatically sent to the resource server. However, in-memory storage has drawbacks: first, it remains susceptible to XSS attacks, though the risk is lower than with local storage; second, tokens are lost when users close browser tabs, leading to a degraded user experience as reauthentication is required.
Authorization Code with PKCE Flow
To address security concerns fundamentally, modern SPAs should avoid the obsolete Implicit flow and instead adopt the Authorization Code with PKCE (Proof Key for Code Exchange). This flow enhances security through dynamically generated code_verifier and code_challenge. In this model, access tokens can be securely stored in memory, while the use of refresh tokens can be minimized or even avoided entirely.
Code Example: Implementing Secure Token Management
Below is a simplified example demonstrating how to store access tokens in memory in an SPA, combined with the PKCE flow:
// Generate code_verifier and code_challenge for PKCE
function generatePKCE() {
const codeVerifier = generateRandomString();
const codeChallenge = base64UrlEncode(sha256(codeVerifier));
return { codeVerifier, codeChallenge };
}
// Store access token in memory
let accessToken = null;
function setAccessToken(token) {
accessToken = token;
}
function getAccessToken() {
return accessToken;
}
// Perform silent sign-in when token expires
async function silentSignIn() {
// Reacquire token using PKCE flow
const pkceData = generatePKCE();
const response = await fetch('/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code_verifier: pkceData.codeVerifier })
});
const data = await response.json();
setAccessToken(data.access_token);
}
Advantages of Using Existing Libraries
Implementing a full authentication flow from scratch is complex and prone to security vulnerabilities. Therefore, it is recommended to use mature libraries like oidc-client-js, which are extensively tested and handle details such as PKCE and token refresh correctly. For instance, oidc-client-js offers built-in token management features, automating silent sign-in and reducing developer burden.
Supplementary Approach: Path-Specific Cookie Storage
Another option is to store refresh tokens in HttpOnly cookies but assign them a specific path (e.g., /refresh). This ensures that refresh tokens are only sent with requests matching that path, avoiding exposure in regular API calls. This method balances security and persistence to some extent but still requires attention to CSRF protection.
In summary, when storing refresh tokens in SPAs, security should be prioritized over convenience. Adopting the Authorization Code with PKCE flow, combined with in-memory storage and the use of existing libraries, represents the most effective strategy today. Developers should regularly update their knowledge to keep pace with evolving threats in OAuth and web security.