Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh token rotation gets run twice with the middleware enabled, and then fails because the token has been revoked #11240

Closed
Vinnl opened this issue Jun 23, 2024 · 1 comment
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@Vinnl
Copy link

Vinnl commented Jun 23, 2024

Environment

  System:
    OS: Linux 6.8 Fedora Linux 40 (Container Image)
    CPU: (8) x64 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
    Memory: 5.51 GB / 15.33 GB
    Container: Yes
    Shell: 5.2.26 - /usr/bin/bash
  Binaries:
    Node: 20.11.0 - ~/.volta/tools/image/node/20.11.0/bin/node
    Yarn: 1.22.19 - ~/.volta/tools/image/yarn/1.22.19/bin/yarn
    npm: 10.2.4 - ~/.volta/tools/image/node/20.11.0/bin/npm
    pnpm: 8.14.2 - ~/.volta/bin/pnpm
  npmPackages:
    next: 14.2.3 => 14.2.3 
    next-auth: ^5.0.0-beta.18 => 5.0.0-beta.18 
    react: ^18 => 18.3.1 

Reproduction URL

https://github.com/Vinnl/next-auth-bug-repro/tree/refresh-token-rotation-pkce

Describe the issue

I'm not entirely well-versed in OAuth, so please bear with me.

I've set up the Spotify provider in a Next.js app using auth.js, and tried to implement refresh token rotation as described in the guide. I'm using JWT, not a database, to maintain a user session.

As I understand it (I think this is a PKCE thing?), the refresh token gets invalidated after use - and along with the new access token, you also get a new refresh token for the next rotation.

However, if the Next-Auth middleware is enabled, it appears to attempt to rotate the same refresh token multiple times, the first of which is successful, but after that resulting in errors because the token has now been revoked.

How to reproduce

  1. git clone --branch refresh-token-rotation-pkce [email protected]:Vinnl/next-auth-bug-repro.git
  2. Create a client ID and secret on https://developer.spotify.com/dashboard (Spotify Premium required, unfortunately).
  3. Set AUTH_SECRET, AUTH_SPOTIFY_ID and AUTH_SPOTIFY_SECRET in .env.local.
  4. pnpm install
  5. pnpm run dev
  6. Sign in using Spotify
  7. Wait for two minutes (that's the time I set up to wait before refreshing the token)
  8. Refresh the page

Expected behavior

The JSON output showing the current session gets:

  "error": "RefreshAccessTokenError"

and if you look at the terminal in which you ran pnpm run dev, you'll see something like:

Refreshed token: {
  access_token: '<snip>',
  token_type: 'Bearer',
  expires_in: 3600,
  refresh_token: '<snip>',
  scope: 'streaming user-modify-playback-state user-read-email user-read-private'
}
Could not refresh access token: { error: 'invalid_grant', error_description: 'Refresh token revoked' }
Could not refresh access token: { error: 'invalid_grant', error_description: 'Refresh token revoked' }

If you now were to make any API calls with the access token shown on the page, they'd get rejected.

The expected behaviour is for the session to contain a valid access token, not have the error, and for the logs to not show the "Refresh token revoked" error. And in fact, that is what seems to happen if you rm middleware.ts, I think?

@Vinnl Vinnl added bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Jun 23, 2024
@abencun-symphony
Copy link

I've resolved this by doing this:

In the middleware call auth but then inject the session via a request header:

if (session) {
  headers.set('X-Serialized-Session', btoa(encodeURIComponent(JSON.stringify(session))));
}

In your components, route handlers, wherever, just write and consume a helper method to deserialize and use the session:

import { headers } from 'next/headers';

export function readSession() {
  const encodedSession = headers().get('X-Serialized-Session');
  const decodedSession = typeof encodedSession === 'string' ? decodeURIComponent(atob(encodedSession)) : undefined;
  return decodedSession;
}

This absolutely prevents multiple calls to auth() that result in multiple token refreshes since only middleware is allowed to invoke it.

@nextauthjs nextauthjs locked and limited conversation to collaborators Jul 2, 2024
@balazsorban44 balazsorban44 converted this issue into discussion #11317 Jul 2, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests

2 participants