nextjs-auth0
nextjs-auth0 copied to clipboard
Documentation to use refresh tokens is missing some information
Checklist
- [X] I have looked into the README and have not found a suitable solution or answer.
- [X] I have looked into the examples and have not found a suitable solution or answer.
- [X] I have looked into the API documentation and have not found a suitable solution or answer.
- [X] I have searched the issues and have not found a suitable solution or answer.
- [X] I have searched the Auth0 Community forums and have not found a suitable solution or answer.
- [X] I agree to the terms within the Auth0 Code of Conduct.
Describe the problem you'd like to have solved
I tried setting up refresh tokens following the instructions (adding offline_access
to my AUTH0_SCOPE
environment variable), and I thought it was working. However once the token expired, I would see issues when trying to use the getAccessToken
method on my API route.
It wasn't exactly clear that I hadn't setup refresh tokens correctly because the token was being issues with all of the other scopes.
In the end I had noticed that on Auth0 I hadn't enabled refresh tokens for my API, but I did enable them for my application.
Describe the ideal solution
- Better documentation, explaining that one must:
- include
offline_access
scope in their scope list (either environment variableAUTH0_SCOPE
or provided manually) - enable refresh token grant type for their application:
- allow offline access for their API(s) associated with the application
- include
- Throw error when granted token doesn't include all the requested scopes so that one can find the issue
Alternatives and current workarounds
No response
Additional context
No response
Also - one issue I noticed is that the withPageAuthRequired
wasn't redirecting a user to login when they had an expired session/JWT, but would fail on the getAccessToken
method.
One would expect that withPageAuthRequired
would redirect users when they have an expired session
Thanks for the suggestions @joelzwarrington
Better documentation, explaining that one must:
We include the configuration needed for the client in the example here https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#access-an-external-api-from-an-api-route
But agree that we could also include some information about setting up the tenant (we do have that information here also https://auth0.com/docs/secure/tokens/refresh-tokens#:~:text=If%20you%20want%20to%20allow,for%20a%20new%20access%20token.)
withPageAuthRequired wasn't redirecting a user to login when they had an expired session/JWT, but would fail on the getAccessToken method.
withPageAuthRequired
doesn't use the getAccessToken
method, so this should not happen (and I can't reproduce this on the example app)
Can you share some code that reproduces your issue?
hey @adamjmcgrath, thanks for reaching out, I'll look to setup an example which reproduces my issue.
But if you want to try and reproduce:
- refresh token grant enabled on application
- offline access disabled on api
- scopes:
offline_access openid profile email
with that setup, withPageAuthRequired
would not redirect when token was expired and wouldn't get an updated token. when I would use getAccessToken
in an API route which was called from the page that had withPageAuthRequired
, the getAccessToken
function would error that the token was expired and couldn't get refresh token.
I'll try and get an example together for you by next Monday
withPageAuthRequired would not redirect when token was expired
Don't worry about the example, this is expected behaviour. The duration of the session is not tied to the expiry of the access token. Access Tokens tend to be short lived (the default for auth0 is 1 day) and the session duration for this SDK is longer (the default is 1 day rolling, 7 days absolute)
@adamjmcgrath what's the difference between session and access token?
So it's possible for someone to have a logged in session, but an expired access token?
@adamjmcgrath what's the difference between session and access token?
Am going to borrow this explanation, since it's pretty thorough https://github.com/nextauthjs/next-auth/issues/693#issuecomment-696696671
So it's possible for someone to have a logged in session, but an expired access token?
Correct, if you're not refreshing your access token and the access token expires before the absolute duration of the session then you can have a logged in session with an expired access token.
I see, thank you for sharing that explanation. It would be nice for these to be documented in some way, although I don't know what the best way is.
As you mentioned there is some documentation on auth0.com/docs, but better discoverability would be ideal.
Here are some things I might suggest:
- Adding some of this information to the EXAMPLES.md
- Updating/adding quick start guide for Nextjs + refresh tokens (Nextjs guide)
- Adding further examples for refresh token flows (and what to configure on your Auth0 app + api)
It's a bit of a vague ask so feel free to close as my issue has been resolved, but I thought it might help others who encounter the same issue as me.
hey @adamjmcgrath, thanks for reaching out, I'll look to setup an example which reproduces my issue.
But if you want to try and reproduce:
refresh token grant enabled on application
offline access disabled on api
scopes:
offline_access openid profile email
with that setup,
withPageAuthRequired
would not redirect when token was expired and wouldn't get an updated token. when I would usegetAccessToken
in an API route which was called from the page that hadwithPageAuthRequired
, thegetAccessToken
function would error that the token was expired and couldn't get refresh token.I'll try and get an example together for you by next Monday
@joelzwarrington i am running into this issue as well.
We use withMiddlewareAuthRequired to protect all routes (pages and API). The middleware only checks if there is a valid session but not if the access token is valid.
We have an API proxy route which proxies calls to our backend API and attached an access token (since these aren't accessible client side). When an API route is reached (passing middleware check because the session exists) it uses getAccessToken
. This works for some time but eventually it starts throwing this error
[AccessTokenError: The request to refresh the access token failed. CAUSE: invalid_grant (Unknown or invalid refresh token.)]
I tried using getSession
in the API route and noticed it does have an access token but it is invalid.
I also noticed that if I manually navigate to /api/auth/login it fixes the issue without requiring the user to authenticate again (it goes to the universal login page then immediately redirects back to the app).
For our use case a session is effectively 1:1 with an access token - the app only works by making calls to the backend.
I have 2 questions
- How do I fix this intermittent error when trying to retrieve an access token in the api route?
- Is it possible to modify the middleware auth so it confirms both a session and valid access token (otherwise redirecting to /api/auth/login)?
@ci-vamp yeah, that's the same issue I encountered.
can you confirm that you've allowed the refresh token grant type on your app in auth0?
You'll also need to enable offline access in your API configuration. This is what the issue was for me, once I did that, I could use 'getAccessToken' within my API route (the one that acted like a proxy to my GraphQL API). I did also use 'withApiAuthRequired' on the route.
Oh, you'll also need to use the oauth scope for refresh tokens (I used env variables to do this)
@joelzwarrington
i believe everything is wired up correctly
Application settings
(i dropped down all the expiration times so i could debug this)
API settings
AUTH0_*
env vars
AUTH0_BASE_URL=$NEXT_PUBLIC_FRONTEND_BASE_URL
AUTH0_SCOPE='openid profile email offline_access'
AUTH0_API_AUDIENCE=<API > general settings > audience identifier>
AUTH0_SECRET=<random hash for signing cookies>
AUTH0_CLIENT_ID=<Application client ID>
AUTH0_CLIENT_SECRET=<Application client secret>
AUTH0_ISSUER_BASE_URL=<issuer>
in my API proxy i call
const { accessToken } = await getAccessToken(req, res);
and it ends up throwing
AccessTokenError: The request to refresh the access token failed.
CAUSE: invalid_grant (Unknown or invalid refresh token.)
when i check the a0 logs i get
{
"date": "2023-09-10T17:26:56.043Z",
"type": "fertft",
"description": "Unknown or invalid refresh token.",
"connection_id": "",
"client_id": "< AUTH0_CLIENT_ID>",
"client_name": "<Application name>",
"ip": "<my IP>",
"user_agent": "Other 0.0.0 / Other 0.0.0",
"hostname": "<AUTH0_ISSUER_BASE_URL>",
"user_id": "",
"user_name": "",
"auth0_client": {
"name": "nextjs-auth0",
"version": "2.7.0",
"env": {
"node": "v16.20.0"
}
},
"log_id": "90020230910172656898920000000000000001223372051076189986",
"_id": "90020230910172656898920000000000000001223372051076189986",
"isMobile": false,
"id": "90020230910172656898920000000000000001223372051076189986"
}
To be clear, in your configuration the refresh tokens will expire 30 seconds after login (as it's set as absolute).
You might want to try increasing the duration for the refresh token in your tests.
Are you wrapping your API proxy in 'withApiAuthRequired'?
Other than that, I can't spot anything wrong with your configuration.
Perhaps you might want to submit a bug or get in contact with auth0 support?
Yes that was for debugging. I have tried every combination I could think of. Increasing the expiration (out to the max of 365.5 days) delays it but eventually it occurs again.
I don't have withApiAuthRequired because I am protecting globally with the middleware variant. I also tried moving the call to the middleware to see if that was related but it occurs there too. In fact the reason I moved to trying the middleware was because originally I wrapped each page and route individually and I thought using the middleware would fix it. Or at least I'd be able to redirect on the error (but this also doesn't work - I run into CORS issues trying to redirect from a api proxy call to the login page).
What I have tried:
- RTR from 0 to 90 seconds
- RTR disabled
- absolute expiration from 30s to 365.5 days - the longer the period the longer it works before hitting the error again. At 30s I can hit it consistently
- disabling absolute expiration - this is not acceptable for our service but it did fix the issue
- getting access token in the api proxy
- getting access token in the middleware and sending it over through an x-header (had to upgrade to 3.1 to try this)
- adding
refresh: true
togetAccessToken
- using the top level
scope
(as an array) instead of in the auth params (as space separated list)
Every time I changed a0 settings I waited 1 min and did a fresh logout + log in.
What I have noticed:
- when the call fails with the error described above, the a0 logs show the ftrr error with no connection listed.
- extending / disabling absolute expiration seems to work. Despite having RTR disabled i saw constant (dozens) of refresh token success logs.
- when they succeed it has the connection name and ID. I don't know what this means
- if I forcibly log in it (redirecting to /api/auth/login from middleware) fixes it. But this only works on page routes. For api routes I get a CORS error from a0 so I can't use this solution.
I have read every a0 community and GH issue related to this subject - everything is purple. I have tried every recommended setting and approach. I’m really at a loss.
The only thing I can think of is that the api proxy is trying to create an access token for every request that comes in. This leads to some overload of the token endpoint for data heavy pages (our heaviest makes 35 calls on load).
Ideally I would use the access token on the session then refresh as it approaches expiration ("silent auth" as it was done in a0 spa lib) but I can't figure out how to do this without redirecting to the login page periodically (see observations above).
We recently migrated from a SPA to nextjs and are preparing for a prod rollout in a few weeks. This bug is now putting it on hold. Because of its inconsistent nature I can't give the green light (or go into prod with absolute lifetime disabled).
@adamjmcgrath Any way you can help me on this? I've done the most diligence I can and spent the past 4 days grinding on it.
Hi @ci-vamp - this issue was closed a while ago.
If you think there's a problem with the SDK, please raise a new issue with steps to reproduce and we can take a look.
RTR disabled
Also, ☝️ this is the better setting for Web Applications (concurrency is difficult to handle in web apps and concurrent requests from different servers can trigger breach detection - which results in ferrt errors like the errors you have) - RTR is generally for SPAs
@adamjmcgrath I will try to put together a reproducible example without using our source code (can't be shared).
In the mean time can you help me understand something. It seems the behavior is tied to an expiring refresh token (evidenced by dropping expiration to 30s).
My fear of putting a longer absolute expiration is that it's a ticking time bomb. It will work for a while but when it expires this issue will arise.
Is a session tied to a refresh token? If not then what determines when a session expires?
What is the recommended way of handling this issue when the refresh expires?
@ci-vamp hello, did you find a solution/workaround for this problem? We are dealing with the same.
@ci-vamp asking as well, if you dealt with the problem.
Our situation is pretty much 1:1 with what you've been describing. We moved from Gatsby SPA front end silent auth that was working fine getting tokens for our external API, now to Next JS. I've tried using regular server side functions, then API routes, and middleware, as I thought read-only cookies on SSR were the issue but to no avail, all have the same refresh token issue.
I think it doesn't fail on the first refresh but rather on the subsequent. The token gets invalidated but the session persists so user is no longer receiving data even though it appears he's logged in and everything is fine.
Tried tons of options and variations of settings, keep updating to latest libs, but nothing really gets me there. Trying to debug this from time to time the past 2-3months now as the release deadline approaches.
Ultimately my guess is it could be that the next js lib is sending multiple refresh token requests at same time and it does work on the first request, but then immediately after it wants to refresh token while using token that was just used and it blocks out any further requests. I don't really see how I could prevent that from happening though.
Would appreciate any inputs
hey @punksta / @EvGreen I'd suggest writing a new issue describing your problem and how to reproduce so that the library maintainers can see it. :)