Action to copy app_metadata to tokens not working

Users have app_metadata. I need to surface that in the id token and the access token so that it can be read when accessing my API but also in my NextJS middleware, for access control.

I’ve written the following rule:

exports.onExecutePostLogin = async (event, api) => {
  const {
    user: {
      app_metadata
    }
  } = event;

  api?.idToken?.setCustomClaim?.(`app_metadata`, app_metadata);
  api?.accessToken?.setCustomClaim?.(`app_metadata`, app_metadata);

};

…but it doesn’t seem to add the information to the token! Does anyone know why??

Any help greatly appreciated! :slight_smile:

Hey @james.sherry !

I just tested this code on my end and it does indeed add the existing app_metadata as a custom claim in both access and id tokens. Can you confirm you have the action deployed and added to the login flow?

Sorry, U think I was treating them like rules and believed once you’d set one to a trigger (because function name) that it would automatically fire. I hadn’t looked at the Flow panel - I suspected it was pre-written stuff.

My bad and Thank you! :slight_smile:

1 Like

Thanks for confirming!

Sorry, @tyf ,
I’ve just noticed that it passes through to the access token but not the ID token!

My middleware.ts is like:

import { NextResponse } from "next/server";
import {
  withMiddlewareAuthRequired,
  getSession,
} from "@auth0/nextjs-auth0/edge";
import type { NextRequest } from "next/server";

// import Router from "next/router";

const AUTH0_NAMESPACE = process.env.NEXT_PUBLIC_AUTH0_NAMESPACE;

export default withMiddlewareAuthRequired(async function middleware(
  req: NextRequest
) {
  try {
    const res = NextResponse.next();
    const user = await getSession(req, res);

    console.log("user", user); 
    
    return res;
  } catch (err) {
    console.log("in error", err);
    // If not logged in
    NextResponse.redirect(new URL("/api/auth/login", req.url));
  }
});

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * * - site.webmanifest (manifest file)
     */
    "/((?!api/auth|_next/static|_next/image|favicon.ico|site.webmanifest).*)",
  ],
};

I get a response like:

Session {
  user: {
    given_name: 'Joe',
    family_name: 'Bloggs',
    nickname: 'joe.bloggs',
    name: 'Joe Bloggs',
    picture: 'https://lh3.googleusercontent.com/a/ACg8ocK-otc-TRx8Nr21UmyGoNTZmzGbeimmn_UNJbu6r0B=s96-c',
    locale: 'en-GB',
    updated_at: '2023-11-13T21:48:46.459Z',
    email: 'service.user@domain.com',
    email_verified: true,
    sub: 'google-oauth2|1005043426187610930',
    sid: 'obYPmBAK9gGJThSIbqaLI50PsnVXlEc8'
},
  accessToken: <access token>,
  accessTokenScope: 'openid profile email',
  accessTokenExpiresAt: 1699998526,
  idToken: <id token>
  token_type: 'Bearer'
}

Any idea why?

(FYI Japp_metadata is a typoI made here, it’s not in the codebase) Rule is like:

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://portal.thejump.tech';

  const {user} = event;

  if(user?.app_metadata){
    const {app_metadata} = user;
    api?.idToken?.setCustomClaim?.(`${namespace}/app_metadata`, app_metadata);
    api?.accessToken?.setCustomClaim?.(`${namespace}/app_metadata`, app_metadata);
  }
  if(user?.user_metadata){
    const {user_metadata} = user;
    api?.idToken?.setCustomClaim?.(`${namespace}/user_metadata`, user_metadata);
    api?.accessToken?.setCustomClaim?.(`${namespace}/user_metadata`, user_metadata);
  }

};

thanks

1 Like

No worries, happy to help!

Hmm… Are you able to extract the ID token at all and inspect it at jwt.io? What if you console.log something like const idTokenClaims = session.idToken?

Are you able to extract the ID token at all and inspect it at jwt.io?

I am. It looks like:

{
  "given_name":"Joe",
  "family_name":"Bloggs",
  "nickname":"joe.bloggs",
  "name":"Joe Bloggs",
  "picture":"https://lh3.googleusercontent.com/a/ACg8ocK-otc-TRx8Nr21UmyGoNTZmzGbeimmn_UNJbu6r0B=s96-c",
  "locale":"en-GB",
  "updated_at":"2023-11-13T21:48:46.459Z",
  "email":"service.user@domain.com",
  "email_verified":true,
  "iss":"https://dev-24zlwfwraop7nz.uk.auth0.com/",
  "aud":"RbZMttNmP5DLNy1ycXyQ9Dcv4AZxdA",
  "iat":169991226,
  "exp":169948126,
  "sub":"google-oauth2|10050432678187610930",
  "sid":"obYPmBAK9gGThSIbqaLI0PsnVXlEc8",
  "nonce":"OCvpF7pt_njsdBSA54kqc9CqemJg921RjWJUjLLfU"
}
1 Like

Strange! I’m actually seeing the same behavior in our sample app. I will dig into this a bit deeper and let you know what I come up with :slight_smile: FWIW I am seeing the custom claim(s) when using the useUser hook here:

1 Like

Thanks @tyf !

Sorry, I’m being stupid actually. They are going into the idToken (and are therefore visible with useUser on the front-end); it’s the accessToken, (which you decode with getSession from “@auth0/nextjs-auth0/edge”) that I’m using in middleware redirects, that doesn’t have the claims in. So:

const user = await getSession(req, res); // <-- this user
1 Like

Gotcha, thanks for confirming!

I was using an old version of our sample app which was giving me problems - I just updated to use the latest version of the SDK (3.3.0) and am getting the metadata claims in both the ID token (useUser) and access token (getSession):

Can you confirm that you do not get the access token claims in our sample app?

This is weird and inconsistent and I don’t understand it fully! Hopefully you can help me?

So, I had the brainwave of using the API explorer and manually putting in the ID via getUser. I got:

{
  "created_at": "2023-10-21T20:10:33.144Z",
  "email": "service.user@thejump.tech",
  "email_verified": true,
  "family_name": "User",
  "given_name": "Service",
  "identities": [
    {
      "provider": "google-oauth2",
      "access_token": "ya29.a0AfB_byCqVxCXCL77ehQyIGcHQPXOgtdUZD2pU4E_bwKA0Vrz-nojOf1Q9hWUZYLkCr4MGz78_SNMQFcNVer5tejAUZ1VE_JWBp1usgOSyLypDlmGrAmYsFzbydTZ6rzsTwMiku8yOquVYjHme2rEq5XgqQ6FkQ4_j2gaCgYKAZoSARASFQHGX2Miqf1xXbrRSDVOc3hOJ3XzZg0170",
      "expires_in": 3599,
      "user_id": "100504342678187610930",
      "connection": "google-oauth2",
      "isSocial": true
    }
  ],
  "locale": "en-GB",
  "name": "Service User",
  "nickname": "service.user",
  "picture": "https://lh3.googleusercontent.com/a/ACg8ocK-otc-TRx8Nr21UmyGoNTZmzGbeimmn_UNJebu6r0B=s96-c",
  "updated_at": "2023-11-15T00:13:02.561Z",
  "user_id": "google-oauth2|100504342678187610930",
  "user_metadata": {},
  "app_metadata": {
    "admin": false,
    "courses": [
      "web"
    ],
    "tutors": [
      "james",
      "roger"
    ]
  },
  "last_ip": "152.37.66.1",
  "last_login": "2023-11-15T00:13:02.561Z",
  "logins_count": 25
}

user_metadata and app_metadata but no roles or rbac (the other 2 rules I have in my login flow)

Then I took the sample app above and ran it and copied the env files over (I’m using the pages router where you’re using the app router btw) and I noticed a missing environment variable mentioned for API audience in the boot. I then added that and got:

{
  "https://portal.thejump.tech/roles": [
    "Portal Test Role"
  ],
  "https://portal.thejump.tech/app_metadata": {
    "admin": false,
    "courses": [
      "web"
    ],
    "tutors": [
      "james",
      "roger"
    ]
  },
  "https://portal.thejump.tech/permissions": [
    "homework:create"
  ],
  "given_name": "Service",
  "family_name": "User",
  "nickname": "service.user",
  "name": "Service User",
  "picture": "https://lh3.googleusercontent.com/a/ACg8ocK-otc-TRx8Nr21UmyGoNTZmzGbeimmn_UNJebu6r0B=s96-c",
  "locale": "en-GB",
  "updated_at": "2023-11-15T00:13:02.561Z",
  "email": "service.user@thejump.tech",
  "email_verified": true,
  "sub": "google-oauth2|100504342678187610930",
  "sid": "iQBiUdDuOefgFtk37Fb15_gsAJY_VVKo"
}

(adding and removing the audience seems to make no difference)

Now I have roles, rbac and app_metadata but no user_metadata.

I then put new values in user_metadata and called the API explorer again and it picked them up. Still no look with either my original app or the basic example app.

I tested my action and got confirmation:

[
  {
    "name": "https://portal.thejump.tech/app_metadata",
    "target": "idToken",
    "type": "SetCustomClaim",
    "value": {}
  },
  {
    "name": "https://portal.thejump.tech/app_metadata",
    "target": "accessToken",
    "type": "SetCustomClaim",
    "value": {}
  },
  {
    "name": "https://portal.thejump.tech/user_metadata",
    "target": "idToken",
    "type": "SetCustomClaim",
    "value": {}
  },
  {
    "name": "https://portal.thejump.tech/user_metadata",
    "target": "accessToken",
    "type": "SetCustomClaim",
    "value": {}
  }
]

So it looks like it works in theory! (It’s the one in my comments above)

(Also I can never decode the accessToken on JWT.io to see what values are in. It’s always ‘invalid format’)

So, I’ve got most of the data but I’m really confused as to:

  1. Why it suddenly started to work?! (The only thing I can think of is that calling the API explorer caused it to refresh a cache or something? In the old Rules you had to manually call to cache this data, I think?)
  2. Why it’s not giving me the last piece of data and why the API explorer call does?!

So, I’m really close but can you shed any light on that?

Happy to provide any info you require and many thanks for all your help so far!

Scratch that! I’ve got it working.

I’m not exactly sure, as I did some things to poke cache (like changing the action and redeploying), but I think what I did wrong there was that I hadn’t logged in and out and so the data was available in the API explorer but not the app until I authenticated with the test app. I think that’s where the inconsistency comes from.

(Still no idea why I can’t decode the accessToken though!)

1 Like

Ahh good to know, thanks for the update!

It could be due to a lack an audience param/claim resulting in an opaque access token.

Yeah, Iread that so I put one in but still no joy. (Just an FYI, no action required)

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.