3

I'm building a web app using Spring Boot (backend) and React (frontend). My authentication is based on JWT, with both access token and refresh token.

  • The refresh token is stored in an HTTP-only cookie.
  • The access token has a short lifetime (15 minutes).

I'm unsure where the access token should be stored on the frontend:

  1. localStorage - easy to use but potentially vulnerable to XSS attacks.
  2. In-memory (e.g. React state or a context) - safer, but the token is lost when the user refreshes the page.

In this setup, what is the recommended place to store the access token, and why? Should I keep it in localStorage for persistence, or only in memory for security?

2
  • 1
    Related, but not a duplicate, IMO: softwareengineering.stackexchange.com/q/460416/118878 Commented Nov 11 at 13:34
  • BTW, I like this question, and surprisingly, I'm not finding a dead obvious answer; it's sort of the elephant in the room from a security perspective. I'm working on an answer, but it will take a little bit. This also affects frameworks like Angular, Vue, next.js, and similar frameworks. Commented Nov 12 at 18:35

2 Answers 2

3

What happens when the user refreshes the page? is the refresh token sent? Are you sending the refresh token with every request?

The key idea with a refresh token is that its securely stored on the device and used so that you can have a short expiry on the access token without forcing the user to login multiple times.

It should only be used when the access token has expired, but a naive approach with cookies will mean its sent on every request, negating the whole point of an access token.

My suggested approach is this:

  1. You have a single page application.

  2. Have the authentication api on a different origin.

  3. Store the access token in Http Only cookie for the API origin and the expiry time as a javascript variable.

  4. Normal site usage doesn't refresh the page but calls APIs and dynamically changes the DOM.

  5. API calls have the access token sent in a cookie.

  6. Precheck that the token hasn't expired via the javascript var

  7. If the token has expired or the API call fails. Hit the authentication API to get a new access token. This should automatically include the HTTP Only cookie (for the Auth origin) with the refresh token.

  8. If that fails. Force the user to log on again, getting a new refresh token.

The end result is that the refresh token is only transmitted when the access token has expired (or new login). This mitigates against man in the middle attacks on the refresh token, which would grant an attacker long lived access

The access token is used on all API calls, but has a short expiry, if its intercepted the vulnerability has a time limit.

When a refresh token is used, it should be revoked and replaced with a new refresh token. This lets you detect interception as you will see the attempted use of revoked tokens. Either by the attacker using old tokens, or the user being logged out by the attacker, forcing them to re-login.

-1

✔ Refresh token in HttpOnly + Secure cookie

✔ Access token only in memory (React state, context, Zustand, Redux, etc.)

This architecture is recommended by:

  • OWASP

  • Auth0

  • Netlify + Vercel security guidelines

  • Modern best practices for SPA security

New contributor
Sagar Roy is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.