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)
Can you point to the code you mean here?
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:
isAuthenticated()
.getTokenSilently({ cacheMode: 'off'})
. If you are no longer logged in at Auth0, it will throw alogin_required
error. However, this only works when not using refresh tokens. If you use refresh tokens, you are asking foroffline_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.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:
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 alogin_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.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.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.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 callgetTokenSilently
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?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:
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 callinggetTokenSilently
. I mistakenly assumedisAuthenticated
=true
guaranteesgetTokenSilenty
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"
onAPIX
, andscope="read:logs"
onAPIY
, but notscope=write:logs
onAPIY
, 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 ourgetTokenSilently
method.Regarding isAuthenticated, I do understand that people think it does something different, but the name is
isAuthenticated
, and notisAuthorized
, 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
andtokens
. 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 likeisMaybeAuthenticated
would be appropriate here. I too fell prey to trusting thatisAuthenticated === 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.So
isAuthenticated
really is more likeisMaybeAuthenticatedToAuth0
. I was originally reading it more asisAuthenticatedToMyAPIServer
. 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}
.