Getting Roles for logged in user without having to add the MetaData manually

Hi,
I am new to Auth0. Please be gentle :slight_smile:

I use React and I am trying to get an API response with the role assigned to that specific user via the Users > Roles on the dashboard.

If I call the hook ‘useAuth0().user’, I am getting only some basic info about the user.

Up to now, the only way for me to see the roles in the response by calling useAuth0().user, it’s by adding the metadata manually from the Users > Details > app_metadata and user_metadata dashboard and create a Rule.
I can’t manually add a metadata to each user. I should be able to see the Roles for that user without having to do the manual work.

Here my code:

const { user, getAccessTokenSilently } = useAuth0();
  const [userMetadata, setUserMetadata] = React.useState(null);

  React.useEffect(() => {
    const getUserMetadata = async () => {
      const domain = "dev-abcd.eu.auth0.com";

      try {
        const accessToken = await getAccessTokenSilently({
          audience: `https://${domain}/api/v2/`,
          scope: "read:current_user",
        });

        const userDetailsByIdUrl = `https://${domain}/api/v2/users/${user.sub}/roles`;

        const metadataResponse = await axios.get(userDetailsByIdUrl, {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          }
        });

        console.log('metadataResponse ==> ', metadataResponse)

        const { user_metadata } = await metadataResponse.json();

        setUserMetadata(user_metadata);
      } catch (e) {
        console.log(e.message);
      }
    };

    getUserMetadata();
  }, [getAccessTokenSilently, user.sub]);

My console log returns the following:

{roles: Array(1)}
roles: ["admin"]

but this is happening ONLY because I manually set those values in the Users > Details > app_metadata and user_metadata, as explained above. If I remove them, I get an empty Object.

I found an article which explained the possiblity of using the user’s role api. I ended up following the Auth0 documentation: Auth0 Management API v2

Now my code has changed as follow:

const userDetailsByIdUrl = `https://${domain}/api/v2/users/${user.sub}`;

const metadataResponse = await axios.get(userDetailsByIdUrl, {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
          param: {
            scope: "read:users read:roles read:role_members"
          }
        });

        console.log(metadataResponse)

and the response is:

data:
app_metadata:
    roles: Array(1)
created_at: "2020-12-28T13:41:35.097Z"
email: "matt@example.com"
email_verified: true
identities: [{…}]
last_ip: "2001:8a0:648a:ef00:5d88:f610:5753:f992"
last_login: "2021-02-19T15:02:17.318Z"
name: "matt"
nickname: "matt"
picture: "https://s.gravatar.com/avatar/a097a0847c295822c6e5407ed3257149?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fma.png"
updated_at: "2021-02-19T18:26:51.634Z"
user_id: "auth0|abcd1234"
user_metadata:
    roles: Array(1)

Even if the response is much better, it’s still not correct.
First, I haven’t used the /roles api, which means I am getting only the data for the specific user ID.
Still, the metadata are those hardcoded in the dashboard.

If I change the API url and add /roles, i get an error: Request failed with status code 403
It means : Insufficient scope; expected all of: read:users, read:roles.

I don’t know what to do anymore. Any help is much appreciated.
Matt

By the way, I forgot to mention. This is the script I’ve added to Rules:

function (user, context, callback) {
  // This namespace can be whatever you want - it will be the 'key' that holds
  // an array of roles on the users token
  const namespace = 'userRole';
  const assignedRoles = (context.authorization || {}).roles;
  
  console.log(context);
  console.log(context.authorization);
  console.log(assignedRoles);

  let idTokenClaims = context.idToken || {};
  let accessTokenClaims = context.accessToken || {};

  idTokenClaims[`${namespace}/roles`] = assignedRoles;
  accessTokenClaims[`${namespace}/roles`] = assignedRoles;

  context.idToken = idTokenClaims;
  context.accessToken = accessTokenClaims;

  callback(null, user, context);
}

Hi @mattbara !

In general, you’d avoid making requests to the Management API v2 from Single Page Apps, unless it’s an app that specifically needs to manage user details and update values on user_metadata.
Instead, when you want an application to receive additional user information that is not part of the OIDC profile (like the roles) as a custom claim in the ID token. Check the doc for more details, but you’d be creating a rule that looks like this:

function(user, context, callback) {
  // the claim namespace doesn't have to resolve to a real resource, but 
  // it should be a valid URL, and it can't be an Auth0 domain
  const claimNamespace = "http://yourapp.com/claims/";

  // user.roles comes pre-populated from the roles assigned
  // via the dashboard or API
  context.idToken[claimNamespace + "roles"] = user.roles;

  callback(null, user, context);
}

If seems that you’ve tried this before, but maybe the namespace was invalid. Can you give this a try?

Your user object will have the roles in the selected claim full name. E.g.

{
  "user_id": "xxxx",
  "email":"xxx",
  "http://myapp.com/claims/roles": ["admin", "manager"]
}

So that you can have:

const ROLES_CLAIM = "http://myapp.com/claims/roles";
var userRoles = user[ROLES_CLAIM];

Hope that helps!

Hi @nicolas_sabena ,

Thanks for answering. I am not sure I am doing the right thing here.

  1. I’ve created a new ‘Rule’ by using your code.
  2. I’ve changed the claimNamespace URL with an existing one, but not related to Auth0 (as per your comment)
  3. Saved
  4. Clicked the button ‘Try this Rule’. Unfortunately the test doesn’t show it

At this stage I decided to test it in my API call. Unfortunately nothing that looks like the object you’\ve metioned is displayed in my API response. The response structure is still the same as the one I’ve mentioned in the original post.
I think I am doing something wrong and the ‘Rules’ are not linked.

The most annoying thing is the ‘Try this Rule’ button never works for me. It always returns the same structure with no changes. I have tried 10 different Rules.

Let me know what you think.
Thanks
Matt

HI @nicolas_sabena ,

Any other ideas? This is not working. It doesn’t matter how I put it, the api doesn;'t return anything you’ve mentioned.

Thanks
M

Hey @mattbara
“Try this rule” is very limited, in that it acts based on a fixed input (the user and object shown in the dialog) and you can’t really see the ID token that the server would issue. You only see the resulting context and user objects after the rule finishes executing.

If you do this to add roles to the ID token:

function(user, context, callback) {
  // the claim namespace doesn't have to resolve to a real resource, but 
  // it should be a valid URL, and it can't be an Auth0 domain
  const claimNamespace = "http://yourapp.com/claims/";

  // user.roles comes pre-populated from the roles assigned
  // via the dashboard or API
  context.idToken[claimNamespace + "roles"] = user.roles;

  callback(null, user, context);
}

and this is not working for you, I would first try to understand if the problem is with the user’s roles not being present, or with the claims in the token.

You can start by putting a fixed set of roles:

function(user, context, callback) {
  const claimNamespace = "http://yourapp.com/claims/";

  context.idToken[claimNamespace + "roles"] = ["manager", "user"];

  callback(null, user, context);
}

Once you have that in place, get the ID token received in the React App, and decode it using https://jwt.io. See if the token has the expected roles claim.

1 Like

Thanks for sharing those tips Nico!

I have found the cause of the issue. It was the URL I was pointing to:

const userDetailsByIdUrl = `https://${domain}/api/v2/users/${user.sub}`;

useAuth0() would not return the user Roles by using that. So, I switched to:

const userDetailsByIdUrl = `https://${domain}/userinfo`;

Now I don’t have to use anymore metadata object.

Now my code looks like this:

React.useEffect(() => {
    const getUserRoles = async () => {
      const domain = "dev-h3vh96po.eu.auth0.com";

      try {
        const accessToken = await getAccessTokenSilently({
          audience: `https://${domain}/api/v2/`,
          scope: "read:current_user",
        });

        const userDetailsByIdUrl = `https://${domain}/userinfo`;

        axios.get(userDetailsByIdUrl, {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        })
        .then(response => {
            setLSValue(LS_KEYS_PRIVATE.USER_TYPE, response.data["https://abc.com/roles"][0]); // this is to set a localstorage
        })
        .catch(error => {
          console.log('ERROR API RESPONSE => ', error)
        });
        
      } catch (e) {
        console.log(e.message);
      }
    };

    getUserRoles();
  }, [getAccessTokenSilently]);
1 Like

Thanks for sharing that with the rest of community!