auth0 / nextjs-auth0

Next.js SDK for signing in with Auth0
MIT License
2.03k stars 382 forks source link

Documentation to use refresh tokens is missing some information #833

Closed joelzwarrington closed 1 year ago

joelzwarrington commented 2 years ago

Checklist

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

  1. Better documentation, explaining that one must:
    1. include offline_access scope in their scope list (either environment variable AUTH0_SCOPE or provided manually)
    2. enable refresh token grant type for their application: image
    3. allow offline access for their API(s) associated with the application image
  2. 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

joelzwarrington commented 2 years ago

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

adamjmcgrath commented 2 years ago

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?

joelzwarrington commented 2 years ago

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:

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

adamjmcgrath commented 2 years ago

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)

joelzwarrington commented 2 years ago

@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 commented 1 year ago

@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.

joelzwarrington commented 1 year ago

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:

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.

ci-vamp commented 1 year ago

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

@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

  1. How do I fix this intermittent error when trying to retrieve an access token in the api route?
  2. Is it possible to modify the middleware auth so it confirms both a session and valid access token (otherwise redirecting to /api/auth/login)?
joelzwarrington commented 1 year ago

@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.

joelzwarrington commented 1 year ago

Oh, you'll also need to use the oauth scope for refresh tokens (I used env variables to do this)

ci-vamp commented 1 year ago

@joelzwarrington

i believe everything is wired up correctly

Application settings

(i dropped down all the expiration times so i could debug this)

image

image

image

API settings

image

image

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>
ci-vamp commented 1 year ago

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

image

{
  "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"
}
joelzwarrington commented 1 year ago

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?

ci-vamp commented 1 year ago

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:

Every time I changed a0 settings I waited 1 min and did a fresh logout + log in.

What I have noticed:

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.

adamjmcgrath commented 1 year ago

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.

adamjmcgrath commented 1 year ago

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

ci-vamp commented 1 year ago

@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?

punksta commented 4 months ago

@ci-vamp hello, did you find a solution/workaround for this problem? We are dealing with the same.

EvGreen commented 4 months ago

@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

joelzwarrington commented 4 months ago

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. :)