Best Practices on PKCE Token Storage in Nuxt Static Site

We have sandboxed an initial auth flow for our Nuxt static site using the Nuxt Auth module (v5) with the PKCE implementation. Although we have an initial login working, some key questions remain on best practices for how to store the token. Auth0 currently returns us a token that is stored in memory only, and thus as expected it does not persist between browser tabs/refreshes.

I’ve searched extensively on Auth0’s website and see that the default behavior for an SPA is to store in memory only, but because we are a static site application this isn’t sufficient as the user loses access when they refresh or navigate browser tabs.

I know there are two additional options - localStorage and cookie - but Auth0 doesn’t seem to provide detailed information on getting these set up in a secure manner to prevent XSS/CSRF attacks and I want to make sure we aren’t putting our application at risk by implementing these methods.

My questions can best be summarized as follows:

  • What token storage method is recommended for a Nuxt static web app which will be communicating to various backend services via GraphQL? Should we be storing it in localStorage, a cookie, or both?
  • If we utilize these methods, what are the precautions we need to take to ensure the token is properly protected?
  • Do we invalidate any of the benefits of a PKCE flow by needing to store the token client-side?
  • Is there a way to work with the Auth0 JS SDK to do this for us out of the box (in combination with the Nuxt Auth module), or do we need to roll our own solution here?

Thank you for any insight that can be provided on these questions!

In my opinion, Auth0 could do a little better in guiding users through various options (other than simply saying that you shouldn’t store tokens in local storage or cookies) but you can have a look at the Next.js SDK implementation: The access token (and optionally refresh token) is saved in an HTTP-only cookie, and the cookie value is encrypted using JSON Web Encryption (JWE, see e.g. jose - npm and nextjs-auth0/cookie-store.ts at b3e0e99a8257dfb9edaa34ea4f0c6375739687ba · auth0/nextjs-auth0 · GitHub).

Since the client side part never has access to this cookie (which you need to e.g. retrieve data from your backend services (REST, GraphQL, etc.)) you have to proxy all your requests through Next.js API routes. So, these (proxy) Next.js API routes have the following job:

  • Get and decrypt access token from the HTTP-only cookie
  • Optional step (if you use refresh tokens): Use the refresh token to get a new access token if the access token expired
  • Send actual requests to your backend (including the access token in e.g. the “Authorization” header)

(The first two steps are part of the Auth0 Next.js SDK (among other things). If you use Nuxt.js, I’m afraid you may have to build this yourself as there is no equivalent Nuxt.js SDK. I’m not very familiar with Nuxt.js but to my knowledge it has a similar concept to Next.js API routes (see Nuxt - The serverMiddleware Property))

The above is assuming that you’re building an app that also has a server side component (SSR). If your app is purely client side, there are probably no great solutions as local storage and cookies are all vulnerable to XSS. My best guess would be to either to give silent authentication a try (though that has it’s own limitations, especially if your are not using custom Auth0 domains) or to utilize rotating refresh tokens (in combination with short lived access tokens, see Refresh Token Rotation). With the latter, I’d assume that you save the access token in memory only and keep the rotating refresh token in local storage (or in a cookie). This will allow you to keep users logged in—even if they refresh the page—by getting a new access token via the refresh token. If an attacker would get access to the (rotating) refresh token (XSS or similar), they couldn’t do much as the refresh token would only be valid for a very short time (assuming that your access token expiry fairly quickly). The attacker would then also automatically invalidate all (other) refresh tokens (see “reuse detection” in the article above).

1 Like