Custom Claim not showing after token refresh

I’m using the latest version of auth0-nextjs.

Here’s the Custom Action we use to add the claims required by our backend:

exports.onExecutePostLogin = async (event, api) => {
  // Add the authenticated user's email address to the access token
  const namespace = 'https://ourapp.com/';
  api.accessToken.setCustomClaim(namespace + 'email', event.user.email);

  if (event.request.query["org_id"] != null) {
    api.accessToken.setCustomClaim(namespace + 'org_id', event.request.query["org_id"]);
  }
};

My client setup is the default:

export const auth0 = new Auth0Client({
  authorizationParameters: {
    scope: process.env.AUTH0_SCOPE,
    audience: process.env.AUTH0_AUDIENCE,
  },

  // Added this based on:
  // https://community.auth0.com/t/custom-claims-not-showing-up-in-nextjs-session/190622/3
  async beforeSessionSaved(session) {
    return session;
  },
});

My middleware is also pretty standard, but this is how I send the org_id query when needed:

if (!session) {
  const basePath = process.env.APP_BASE_PATH ?? '';
  const loginUrl = new URL(`${basePath}/auth/login`, process.env.APP_BASE_URL);

  if (orgId) {
    loginUrl.searchParams.set('org_id', orgId);
  }

  return NextResponse.redirect(loginUrl);
}

This setup works fine initially: the first token correctly includes the org_id claim.

The problem comes when the token expires.
When I call auth0.getAccessToken() to get a new token using the refresh token, the new access token no longer includes the custom claim.

From what I understand, this happens because the org_id query is not included when the SDK requests a new token from /oauth/token.

:backhand_index_pointing_right: My question: How can I pass the org_id (or ensure the claim is preserved) when refreshing the token using getAccessToken()?

Thanks

Hi @Rama

Thank you for creating another post regarding addressing the issue that you are experiencing.

As mentioned in the previous post, have you tried using the useUser() hook for your application? Claims such as org_id and email should be displayed by default without needing to add them as custom claims, unless your users do not register using an email initially. Also, for the beforeSessionSaved() hook, can you also pass idToken as a parameter to see if it changes anything at all? If not, let me know!

Kind Regards,
Nik

Hi @nik.baleca, thanks for your reply.

I tried adding idToken as a parameter in beforeSessionSaved(), but unfortunately nothing changed.

Let me clarify the issue we’re facing:

  • The org_id is a dynamic UUID that the user selects before login.
  • We add this org_id as a custom claim (NAMESPACE/org_id) in the access token, since our backend uses it to verify user permissions for a given organization (users can belong to multiple orgs with different levels of access).
  • The first generated token has the custom claim and everything works fine.
  • However, once the token expires and we request a new one using getAccessToken(), the refreshed token no longer includes the NAMESPACE/org_id. Because of this, the new token is invalid for backend calls.

Right now, our only workaround is to also expire the session after 5 minutes to force a re-login, but that’s obviously not a viable solution. We’re trying to find a way to make the custom claim persist in the refreshed token.

You also mentioned the useUser() hook, but since this is a server-side context, hooks aren’t applicable. In any case, I don’t see the org_id claim in either the user object or the idToken payload.

Kind Regards,
Rama

Hi again!

As far as I have checked with your implementation regarding the matter, it appears that the issue and as you have stated, the issue most definitely is being caused buy the fact that the org_id parameter is not being passed to the request that the SDK is making. Since the SDK makes a direct request to the /oauth/token endpoint, you are unable to pass that parameter.

Regarding the solutions for your current implementation, I would advise to force re-authentication for these users( which might not be ideal) or adapt your action so that the org_id is being added as app_metadata of the user in the initial login. Basically, the code would look something like this:

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://ourapp.com/';
  let orgId;

  // 1. Check for the query parameter on initial login
  if (event.request.query["org_id"]) {
    orgId = event.request.query["org_id"];
    // 2. Persist the org_id to app_metadata for future use
    api.user.setAppMetadata('org_id', orgId);
  } else {
    // 3. If no query parameter, get it from app_metadata (for token refresh)
    orgId = event.user.app_metadata.org_id;
  }

  // Add the org_id claim to the access token if it exists
  if (orgId) {
    api.accessToken.setCustomClaim(namespace + 'org_id', orgId);
  }
  api.accessToken.setCustomClaim(namespace + 'email', event.user.email);
};

Let me know if the proposed solution is suitable or if you have any other questions regarding the matter!

Kind Regards,
Nik