DEV Community

Cover image for Authentication in SPAs: 8 Ways to Handle Tokens and Sessions
Amin Sadeghi
Amin Sadeghi

Posted on

Authentication in SPAs: 8 Ways to Handle Tokens and Sessions

Authentication is one of those things that looks simple until you start thinking about security, token storage, session expiration, and multiple browser tabs.

You can store a JWT in localStorage, keep an access token in memory, use HttpOnly cookies, or introduce a Backend for Frontend (BFF). Each approach comes with different trade-offs.

The important question isn't just how to log users in. It's how to keep them authenticated without exposing their credentials unnecessarily.

1. Memory + Silent Refresh

Keep the access token in JavaScript memory and the refresh token in an HttpOnly cookie.

When the access token expires, the client calls a refresh endpoint. The browser automatically sends the cookie, and the backend issues a new access token.

Trade-off: JavaScript cannot persist the access token across page reloads, so the application needs a refresh flow. The refresh endpoint must also be protected against CSRF where applicable.

2. LocalStorage + JWT

Store the access token in localStorage and attach it to API requests.

const token = localStorage.getItem("access_token");

await fetch("/api/profile", {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
Enter fullscreen mode Exit fullscreen mode

This is straightforward, but any successful XSS attack that executes JavaScript in your origin may expose the token. Long-lived credentials in localStorage increase the potential impact.

3. SessionStorage + JWT

sessionStorage keeps tokens separate between tabs and normally clears them when the tab's page session ends.

It still remains accessible to JavaScript, so it does not protect tokens from XSS. Its shorter persistence is a usability and lifecycle trade-off, not a complete security solution.

4. Server-Side Sessions with HttpOnly Cookies

The backend creates a session and sends the browser a session cookie.

The session data lives on the server, often in a shared store such as Redis or a database.

Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax
Enter fullscreen mode Exit fullscreen mode

The browser sends the cookie automatically on matching requests. This avoids exposing the session identifier to ordinary JavaScript, but cookie-based authentication requires appropriate CSRF defenses.

5. JWT in an HttpOnly Cookie

The backend can store a JWT in an HttpOnly cookie rather than returning it for JavaScript to manage.

This reduces direct token access through JavaScript, but it does not make XSS harmless. Malicious scripts may still perform authenticated actions from the compromised page.

Because cookies are sent automatically, CSRF protection and careful cookie configuration remain important.

6. OAuth 2.0 Authorization Code Flow with PKCE

PKCE protects the authorization code flow against authorization-code interception.

It is commonly used for public clients, including browser-based and mobile applications. However, PKCE is not a token-storage strategy by itself. The application still needs to decide how tokens are handled after authorization.

For an application that can keep credentials on a backend, a server-side session or BFF can reduce the amount of sensitive token material exposed to the browser.

7. Backend for Frontend (BFF)

The browser communicates with a backend dedicated to its frontend. The BFF manages access tokens and communicates with downstream APIs.

The browser uses a session cookie instead of handling API access tokens directly.

This can reduce token exposure, but it introduces backend infrastructure and still requires secure session handling, CSRF defenses, and appropriate authorization checks.

8. Refresh Token Rotation

Refresh token rotation issues a new refresh token whenever the current one is used. The previous token is invalidated, and reuse detection can help identify stolen tokens.

This is not an alternative to cookie storage or a BFF. It is a complementary strategy for managing refresh-token lifecycles.

The backend must implement rotation and revocation correctly, including handling concurrent refresh attempts.

Authentication Is Not Authorization

Authentication answers: Who are you?

Authorization answers: What are you allowed to do?

After authenticating a user, the backend still needs to enforce access rules.

Common approaches include:

  • RBAC: Access based on roles such as admin or editor.
  • Permission-based authorization: Fine-grained permissions such as user.read or order.create.
  • ABAC: Decisions based on attributes such as resource ownership or department.
  • Claims-based authorization: Decisions based on trusted claims, with appropriate validation and server-side enforcement.

A decoded JWT is not automatically trustworthy. The backend must validate its signature, issuer, audience, expiration, and other relevant claims. Authorization decisions must be enforced by the server, not just by hiding buttons in the frontend.

Which Approach Should You Choose?

There is no single solution that fits every application.

For a first-party web application where you control the backend, server-side sessions or a BFF are often worth considering. For a standalone SPA that directly calls APIs, access tokens in memory combined with a carefully designed refresh flow may be appropriate.

The decision depends on your architecture, threat model, deployment constraints, and operational requirements.

The real goal is not to find the most complicated authentication setup. It's to choose the simplest architecture that meets your security requirements and implement it correctly.

Top comments (0)