auth0-react: Beta: isAuthenticated true after access and refresh tokens are expired

Describe the problem

isAuthenticated continues to be true when access and refresh tokens are expired. The client then starts making HTTP calls to protected API endpoints because it is expecting a valid access token to be available. However, the client starts receiving HTTP 403 errors because the session is NOT authenticated.

What was the expected behavior?

I would expect that isAuthenticated would be set to false after both the refresh and access tokens have expired.

Reproduction

Clone and follow the readme in this demo NextJS app I created to reproduce the issue: https://github.com/spenweb/auth-for-real

Can the behavior be reproduced using the React SDK Playground?

Environment

  • Version of auth0-react used: ^2.0.0-beta.0
  • Which browsers have you tested in? Chrome (108.0.5359.124)
  • Which framework are you using, if applicable (Angular, React, etc): NextJS (next@13.1.1)
  • Other modules/plugins/libraries that might be involved: Check demo repo package.json

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 29 (14 by maintainers)

Most upvoted comments

Digging through the code of the SDK, I found that checkSession (or getTokenSilently) is checking the Auth0 session token expiration but not checking the application token expiration thus not refreshing my token when expired.

Can you point to the code you mean here?

I found a workaround calling getTokenSilently with a cacheMode: ‘off’ but it completly bypass the cache and call /authorize even if the token is still valid.

That is expected to happen when you disable the cache, so the behavior seems correct. What would you expect when the cache is turned off?

Additionally, I am not sure I understand what you are trying to do, but let me try and elaborate what I think you want in a few situations:

  • If you want to know if the user is authenticated in the application, use isAuthenticated().
  • If you want to know if the user is authenticated at Auth0, use getTokenSilently({ cacheMode: 'off'}). If you are no longer logged in at Auth0, it will throw a login_required error. However, this only works when not using refresh tokens. If you use refresh tokens, you are asking for offline_access, and are telling our SDK you want to disconnect from the Auth0 session. Also know this is something I believe you should not need.
  • If you want to know if the user can call an API (and has a valid access token), use getTokenSilently() (note, no need to bypass the cache here)

Regarding the second bullet, and why I say you do not need it is the following:

  • If you are not using refresh tokens, you should use getTokenSilently() (without disabling the cache). As long as the Access Token is not expired, it will use it from the cache. Once it’s expired, it calls Auth0 (so it only calls it when necessary). If you are no longer logged in, it will throw a login_required and you know the user is no longer logged in to Auth0 (even better our SDK will log the user out of the SDK in this case automatically). The consequence here is that you will only know if you are still logged in to Auth0 once the access token is expired. However, as we recommend short-lived access tokens, this should be a matter of minutes and shouldnt cause any issues.
  • If you are using refresh tokens, you are using offline_access, meaning you are telling our SDK you want to keep using Access Tokens even when no longer logged in to Auth0. That is by design and how refresh tokens work. In this case, there it makes no sense to ask Auth0 if the user is still logged in. The consequence here is that you will continue to have isAuthenticated to be true, regardless of whether or not your refresh token is expired or not.
  • If you are using refresh tokens, and you want to be logged out when the refresh token is expired and the user is no longer logged in to Auth0, ensure to set useRefreshTokensFallback to true (which would then contact Auth0, potentially throwing a login_required error and then logging the user out of the SDK).

So with the above scenario’s, our SDK allows to ensure isAuthenticated reflects the authentication state from Auth0, but it’s unrelated to the availability of the access token, but entirely driven by Auth0 throwing a login_required error.

in essence you are wrapping getTokenSIlently and could just forget about the concept of being authorized and just think in terms of “do I have a token to do what I want”, and rely on our getTokenSilently method.

Yeah, this is a good point. Creating the idea of isAuthorized probably doesn’t really need to happen. At the highest level of our app, where we protect routes, we can just call getTokenSilently and use that as our gatekeeping mechanism, because our use case is really simple: does this person have a token to talk to our API?

happy to improve our documentation, what would you recommend?

For someone like me, who is just tasked with implementing auth and is by no means an auth expert, I (mistakenly) conflated the idea of being “authenticated” with being “authorized”, which you more clearly described in this thread. I think some high-level mention of this in the docs could be useful as I would venture to guess it’s easy for people not steeped in auth to conflate the two.

For example, on this page under the heading “Evaluation the authentication state”, it says:

you want to make sure anyone is able to visit the public page but not the page that is meant for authenticated users only, such as a settings panel or the user profile details. You can decide which content is available by hiding, disabling, or removing it if no user is currently logged in. You do so by checking the result of calling the auth0Client.isAuthenticated() method

When I first read this, I thought “ok cool, if there’s a page with user-specific information that requires they be logged in to see, I can just check isAuthenticated to determine whether I should then call the API to show that”. But where are you getting user-specific information? Very likely from your own API, which means you’re calling getTokenSilently. I mistakenly assumed isAuthenticated = true guarantees getTokenSilenty will give back a token because, well, they’re authenticated.

I suppose more succinctly, it was easy for me to think “user is authenticated” means “I can call my API to get their data”.

For me personally, what would’ve helped was just some additional clarification in the docs that’s really more educational than anything. Something that states what you stated in this thread: authentication !== authorization (maybe this kind of educational material already exists? if so, I didn’t encounter it when implementing our version of auth0).

I opened a PR to add something to the FAQ in the underlying SDK. It’s called out in the FAQ for auth0-react to also read the FAQ for SPA-JS.

Something like that could make sense in your own application if you need some concept of isAuthorized. However, it can become more complicated as the question might become: “Authorized for what”? You can have all kinds of combinations of audiences and scopes, that all result in being authorized for other things.

E.g. you can be authorized for scope="write:messages" on APIX, and scope="read:logs" on APIY, but not scope=write:logs on APIY, which is why getTokenSilently accepts an audience and scope param in the first place.

Sure, you can call it isAuthorized, but in essence you are wrapping getTokenSIlently and could just forget about the concept of being authorized and just think in terms of “do I have a token to do what I want”, and rely on our getTokenSilently method.

Regarding isAuthenticated, I do understand that people think it does something different, but the name is isAuthenticated, and not isAuthorized, so its name indicates it should be used to verify authentication, and not authorization. Having said that, happy to improve our documentation, what would you recommend?

As mentioned above, we considered dropping the concept of isAuthenticated altogether, and only offer a user and tokens. But this wouldnt neccesarily change much, as you can have a user, but not be authorized to call any API’s, most likely causing the same confusion.

I don’t know that we should close this issue as completed. A prop like isAuthenticated should return true if authenticated or false if unauthenticated. I believe a more clear name like isMaybeAuthenticated would be appropriate here. I too fell prey to trusting that isAuthenticated === true meant something that it apparently doesn’t. This is a particularly common problem with users on Safari, because the refresh tokens generally don’t come through and the error needs to be caught. But in those cases when the refresh tokens fail, isAuthenticated remains true. This is confusing and I’d hope for a clearer name on the property.

In general, once the user has authenticated, there is nothing we can use to know if the user isn’t authenticated anymore, apart from contacting Auth0.

So isAuthenticated really is more like isMaybeAuthenticatedToAuth0. I was originally reading it more as isAuthenticatedToMyAPIServer. I am currently only using one audience (one API server) with Auth0.

It would be awesome if there was a convenience method that was part of this package that allowed for checking whether the current session is authenticated for a particular audience. Also, if there was an event or callback triggered when the user session is no longer authenticated due to refresh token expiration when useRefreshTokensFallback={false}.