Rotating refresh token locking users out after expiry

Hi Auth0, I started using Auth0 for a toy project a couple of months ago with 5 or so users (just family). This was around the time when rotating refresh tokens were made available, so I chose to use that for my SPA (React). One by one users (including myself) have encountered an issue whereby a call to the token endpoint results in a 403 and an Error: Unknown or invalid refresh token.

I have so far managed to resolve this by asking users to quite hackily, go into their local storage, and delete the key for the auth0SPA, which then allows them to perform a login. Today, I had the issue happen to me again, and upon looking at the refresh token’s expiry, I realised that it expired a couple of days ago. I then went into my SPA settings on the Auth0 portal, and saw that the lifetime of my refresh tokens is set to 30 days. So i’m now thinking that the 2 are related. I am aware that this can be extended to 3 months, and it’s what I will be doing.

My questions:

  1. Am I correct in understanding that users MUST log in again at least once ever 3 months? I.e. there is no option whatsoever that a user is logged in ad infinitum. Again, I’m ok with this, and understand why it is so for security reasons.
  2. Should my code be the one deleting the Auth0SPA entry in my local storage? This makes little sense to me, since I believe the SPA.js library should be able to handle this by itself. What should my code be doing when the refresh token has expired?

EDIT: While I was reviewing the code, I noticed that I’m using cacheLocation="localstorage" when creating my Auth0Provider. Upon removing this, I was unable to reproduce the issue. The reason I had chosen to specify the cacheLocation as localstorage, was because emitting this would result in the website not working on Safari. Is it possible that the library is not handling refresh token expiry correctly when cache location is localstorage?

Hey @Mark-Buhagiar, let me see if I can help.

  1. Yes that’s correct, our rotating refresh tokens have this default absolute expiry of 30 days, after which your users must log in again

  2. As long as you can handle the invalid refresh token error, instead of deleting local storage, you should just put your users back through an interactive login flow, either by using loginWithRedirect or loginWithPopup. This is the only way you can get a new refresh token, but doing this will also refresh your local storage state so that it contains the correct and valid tokens. No need to delete manually.

^ This last point is something we could be doing better at explaining or showing how it’s done, which I will try to solve.

Also your point about Safari is correct - if you do not use local storage, you are then relying on Auth0 being able to read your Auth0 session cookie, which it is unable to do in browsers like Safari and Brave that block third-party cookies by default.

Hope that helps!

1 Like

The error is being thrown from createAuth0Client. Since there is not a valid client its not possible to redirect or popup a login. The only way around this is to manually delete the token from session storage.

I tried this

try {

				this.auth0Client = await createAuth0Client({
					domain: options.domain,
					client_id: options.clientId,
					audience: options.audience,
					redirect_uri: redirectUri,
					cacheLocation: options.cacheLocation,
					useRefreshTokens: true
				});
			} catch (error) {
				console.log(JSON.stringify(error));

				this.auth0Client = await createAuth0Client({
					domain: options.domain,
					client_id: options.clientId,
					audience: options.audience,
					redirect_uri: redirectUri,
					cacheLocation: options.cacheLocation,
					useRefreshTokens: false
				});
				
				this.auth0Client.loginWithRedirect();
			}

By specifying to not use refresh tokens in the failure case, it does allow me to create a client and cause a login. However this doesn’t clear out the invalid refresh token and now I’m in an infinite login prompt “invalid refresh token” loop.

The only resolution I have found is to do one of 2 things:

  1. set useRefreshTokens to false
  2. set cacheLocation to ‘memory’ instead of ‘localstorage’

Ok, I think I figured out a working solution although this feels really hacky looking at the whole thing

I noted the two places that mattered with // important

try {

				this.auth0Client = await createAuth0Client({
					domain: options.domain,
					client_id: options.clientId,
					audience: options.audience,
					redirect_uri: redirectUri,
					cacheLocation: options.cacheLocation,
					useRefreshTokens: true
				});
			} catch (error) {
				console.log(JSON.stringify(error));

				this.auth0Client = await createAuth0Client({
					domain: options.domain,
					client_id: options.clientId,
					audience: options.audience,
					redirect_uri: redirectUri,
					cacheLocation: options.cacheLocation,
					useRefreshTokens: false   // important
				});

				this.auth0Client.logout();    // important
				await this.auth0Client.loginWithRedirect();
			}

Looking at the Auth0 SPA code on github, I’m pretty sure this works because auth0Client.logout() calls

this.cache.clear(); ClientStorage.remove('auth0.is.authenticated');

Hi Nick, thanks for taking the time to play around with this. I too observed that the SPA code on github has the logout method clean up the data in local storage, however I didn’t consider reattempting to create the Auth0Client with the useRefreshTokens set to false. Like you yourself said though, it’s till quite hacky, and I think the API should be able to handle this by itself. In my code I decided to just remove Auth0’s key from local storage as part of my catch block, but I hope that this will bee addressed down the line.