Auth0 Is returning invalid Refresh tokens

I am fetching a refresh token for a user from Auth0, but when I use the refresh token to refresh the ID and access token, I am getting this error.

{
  "error": "invalid_grant",
  "error_description": "Unknown or invalid refresh token."
}

Token exchange request:

const data = new URLSearchParams({
  grant_type: 'client_credentials',
  code: dto.code,
  client_id: process.env.OAUTH_CLIENT_ID || '',
  redirect_uri: dto.redirect_uri,
  scope: 'openid profile email offline_access',
});

Refresh token request:

const data = new URLSearchParams({
  grant_type: 'refresh_token',
  refresh_token: dto.refresh_token,
  client_id: process.env.OAUTH_CLIENT_ID || '',
  client_secret: process.env.OAUTH_CLIENT_SECRET || '',
  scope: 'openid profile email offline_access',
});

Hi @yahyashareef,

The client_credentials grant type is designed for machine-to-machine communication and does not issue refresh tokens. To get a refresh token for a user, you must use a grant type that involves user authentication, such as the authorization_code grant.

Have a good one,
Vlad

I am using the authentication_code but still having the same error

    const data = new URLSearchParams({
      grant_type: 'authorization_code',
      code: dto.code,
      client_id: process.env.OAUTH_CLIENT_ID || '',
      redirect_uri: dto.redirect_uri,
      scope: 'openid profile email offline_access',
    });

Still returning Unknown or invalid refresh token.

Hi again @yahyashareef,

The error Unknown or invalid refresh token indicates an issue with the refresh token itself or the configuration of your application and API in Auth0. It commonly occurs if offline access isn’t enabled for your API, if you’re re-using a rotated token, or if the token wasn’t issued correctly in the first place.

authorization_code is the right grant for user-centric flows. The error you’re seeing now happens during the second step: when your application tries to exchange the refresh token for a new access token.

This points to a few very likely causes:

  1. Offline Access Not Enabled: Your API (the “Audience” for the token) must be explicitly configured in the Auth0 Dashboard to allow the issuance of refresh tokens.

  2. Refresh Token Rotation: If token rotation is enabled on your application, each time you use a refresh token, it is invalidated and a new one is issued. If your code attempts to use the old token again, it will fail with this exact error.

  3. Missing Client Secret (for Confidential Apps): If your application is a “Regular Web Application,” it’s considered a confidential client and must provide its client_secret during the initial authorization_code exchange. Your code snippet for that exchange is missing it, which could mean a valid refresh token was never issued.

I recommend you generate a completely new authorization code and token set after checking the following settings.

1. Enable “Allow Offline Access” for Your API

This is the most frequent cause of this issue.

  • Go to your Auth0 Dashboard.
  • In the left sidebar, navigate to Applications > APIs.
  • Select the API you are using as the audience for your tokens.
  • Click the Settings tab.
  • Scroll down to the “Access Settings” section and enable the “Allow Offline Access” toggle.
  • Click Save.

2. Handle Refresh Token Rotation

Check if rotation is active and ensure your code handles it correctly.

  • In the dashboard, navigate to Applications > Applications.
  • Select the application you are using.
  • Under the Settings tab, scroll down to Refresh Token Behavior.
  • If Rotation is enabled, you must capture and save the new refresh token that is returned in the body of a successful refresh token exchange. The old one is now invalid.

3. Add the Client Secret to the Token Exchange

If your application is a Regular Web App, the client_secret is required to prove its identity when exchanging the code for tokens.

Your authorization_code grant request should look like this:

const data = new URLSearchParams({
  grant_type: 'authorization_code',
  code: dto.code,
  client_id: process.env.OAUTH_CLIENT_ID || '',
  client_secret: process.env.OAUTH_CLIENT_SECRET || '', // <-- Ensure this is included
  redirect_uri: dto.redirect_uri,
});

The scope parameter is generally not needed in the /oauth/token request, as the scopes were already determined in the initial /authorize call that generated the code.

If you have any further questions feel free to reach out!

Have a good one,
Vlad