Skip to main content
2025-05-3110 min read
DevOps & Security

Adding Permissions to Auth0 ID Tokens via Actions

Adding Permissions to Auth0 ID Tokens via Actions

Getting user permissions into an Auth0 ID token so your application can easily parse them can be a surprisingly challenging task. If you've found yourself struggling with this, possibly even after seeking support, you're not alone. It's a common pain point that can lead to significant frustration, especially when it seems like this should be a straightforward setting. This post is dedicated to sharing a solution that finally worked after much trial and error, piecing together insights from Auth0's ManagementClient documentation.
We'll explore how to leverage the onExecutePostLogin trigger within Auth0 Actions to dynamically fetch a user's permissions (based on their assigned roles) and embed them directly into the ID token. Hopefully, this guide will save you the month of headaches it took to figure this out!

Why Add Permissions to Tokens?

Embedding permissions directly into tokens can streamline authorization checks in your applications. Instead of making separate calls to an authorization server or database to determine what a user can do, your application can inspect the token itself. This can lead to:
  • Reduced Latency: Fewer external calls mean faster response times.
  • Simplified Logic: Your application's authorization logic becomes more straightforward.
  • Stateless Authorization: Services can make authorization decisions independently by validating the token and its claims.

The Auth0 Action: onExecutePostLogin

The onExecutePostLogin Action trigger runs after a user has successfully logged in but before the tokens are issued to the application. This is the perfect place to augment the token with additional information.
Here's the JavaScript code for an Action that adds permissions to the ID token:
javascript
1exports.onExecutePostLogin = async (event, api) => {
2 // Define a namespace for custom claims to avoid collisions
3 const namespace = `https://verify4.com/permissions`;
4 const ManagementClient = require('auth0').ManagementClient;
5
6 // For debugging: Log the namespace
7 console.log(`Namespace for custom claims: ${namespace}`);
8
9 // Initialize the Auth0 ManagementClient
10 // Secrets like AUTH0_DOMAIN, AUTH0_MGMT_CLIENT_ID, and AUTH0_MGMT_CLIENT_SECRET
11 // must be configured in the Action's settings in the Auth0 dashboard.
12 const management = new ManagementClient({
13 domain: event.secrets.AUTH0_DOMAIN,
14 clientId: event.secrets.AUTH0_MGMT_CLIENT_ID,
15 clientSecret: event.secrets.AUTH0_MGMT_CLIENT_SECRET,
16 });
17
18 // For debugging: Log client ID and secret (ensure this is handled carefully in production)
19 // It's generally not recommended to log secrets, even in development,
20 // unless you have strict controls over log access and retention.
21 console.log(`Auth0 Management Client ID: ${event.secrets.AUTH0_MGMT_CLIENT_ID}`);
22 // console.log(`Auth0 Management Client Secret: ${event.secrets.AUTH0_MGMT_CLIENT_SECRET}`); // Be cautious with logging secrets
23
24 // Check if the user has assigned roles
25 if (event.authorization && event.authorization.roles) {
26 try {
27 let permissions = []; // Array to hold all permissions from all roles
28
29 // Iterate over each role assigned to the user
30 for (const roleName of event.authorization.roles) {
31 console.log(`Processing role: ${roleName}`);
32
33 // Fetch all roles from Auth0 to find the ID of the current roleName
34 // This is less efficient than fetching a role by its name directly if the API supports it.
35 // Auth0's Node.js SDK might require fetching all and then filtering.
36 const allRolesResponse = await management.roles.getAll();
37 const allRoles = allRolesResponse.data; // Extract the array of roles
38 // console.log('All available roles:', allRoles); // Can be very verbose
39
40 // Find the specific role by its name
41 const roles = allRoles.filter((role) => role.name === roleName);
42 console.log(`Roles matching '${roleName}':`, roles);
43
44 // Check if the role was found and has a valid ID
45 if (!roles.length || !roles[0].id) {
46 console.error(`Role not found or invalid ID for role name: ${roleName}`);
47 continue; // Skip to the next role if this one is not found
48 }
49
50 const roleId = roles[0].id;
51 console.log(`Role ID for ${roleName}: ${roleId}`);
52
53 // Fetch the permissions associated with this role ID
54 const rolePermissionsResponse = await management.roles.getPermissions({ id: roleId });
55 const rolePermissions = rolePermissionsResponse.data; // Extract permissions data
56 console.log(`Permissions for ${roleName}:`, rolePermissions);
57
58 // Add the permission names to our permissions array
59 // The actual permission strings are in `permission_name`.
60 permissions = permissions.concat(rolePermissions.map((p) => p.permission_name));
61 }
62
63 // Remove any duplicate permissions that might arise if a user has multiple roles
64 // granting the same permission.
65 permissions = [...new Set(permissions)];
66
67 // Set the custom claim in the ID token.
68 // The ID token is typically used by the client-side application.
69 api.idToken.setCustomClaim(`${namespace}`, permissions);
70 console.log(`Successfully set permissions in ID token:`, permissions);
71
72 } catch (error) {
73 // Log any errors encountered during the process
74 console.error('Error getting permissions and setting custom claim:', error);
75 // Depending on your error handling strategy, you might want to:
76 // - Allow login without custom claims.
77 // - Deny login if permissions are critical: `api.access.deny('Failed to retrieve permissions.');`
78 }
79 } else {
80 console.log('No roles assigned to the user or authorization object is missing.');
81 }
82};

Code Breakdown

  1. Namespace: const namespace = \https://verify4.com/permissions`;` Custom claims in Auth0 tokens should be namespaced to avoid collisions with standard OIDC claims. Using a URI (like your domain) is a common practice.
  2. Auth0 Management Client:
    javascript
    1const ManagementClient = require('auth0').ManagementClient;
    2const management = new ManagementClient({
    3 domain: event.secrets.AUTH0_DOMAIN,
    4 clientId: event.secrets.AUTH0_MGMT_CLIENT_ID,
    5 clientSecret: event.secrets.AUTH0_MGMT_CLIENT_SECRET,
    6});
    This initializes the Auth0 ManagementClient, which is necessary to query for roles and their associated permissions. The credentials (AUTH0_DOMAIN, AUTH0_MGMT_CLIENT_ID, AUTH0_MGMT_CLIENT_SECRET) must be stored as "Secrets" in the Auth0 Action's settings.
  3. Role Processing Loop:
    javascript
    1if (event.authorization && event.authorization.roles) {
    2 // ...
    3 for (const roleName of event.authorization.roles) {
    4 // ...
    5 }
    6}
    The code checks if the user has any roles assigned (event.authorization.roles). If so, it iterates through each roleName.
  4. Fetching Role ID:
    javascript
    1const allRolesResponse = await management.roles.getAll();
    2const allRoles = allRolesResponse.data;
    3const roles = allRoles.filter((role) => role.name === roleName);
    4// ...
    5const roleId = roles[0].id;
    To get permissions for a role, we first need its ID. The current code fetches all roles and then filters by roleName to find the matching role object and its id. While functional, for environments with a very large number of roles, this could be inefficient. If a more direct "get role by name" API call is available and suitable, it might be preferred.
  5. Fetching Permissions:
    javascript
    1const rolePermissionsResponse = await management.roles.getPermissions({ id: roleId });
    2const rolePermissions = rolePermissionsResponse.data;
    3permissions = permissions.concat(rolePermissions.map((p) => p.permission_name));
    Once the roleId is obtained, management.roles.getPermissions({ id: roleId }) fetches all permissions assigned to that role. The permission_name from each permission object is then added to the permissions array.
  6. Deduplication and Setting Claim:
    javascript
    1permissions = [...new Set(permissions)];
    2api.idToken.setCustomClaim(\`\${namespace}\`, permissions);
    After processing all roles, [...new Set(permissions)] removes any duplicate permission names. Finally, api.idToken.setCustomClaim() adds the consolidated list of permissions to the ID token under the defined namespace.

Important Considerations

  • Secrets Management: Ensure AUTH0_DOMAIN, AUTH0_MGMT_CLIENT_ID, and AUTH0_MGMT_CLIENT_SECRET are correctly configured as secrets in your Auth0 Action. Never hardcode them.
  • Error Handling: The try...catch block is crucial for gracefully handling potential issues, such as API errors or roles not being found. Decide how your application should behave if permissions cannot be fetched (e.g., deny login, log an error and proceed without permissions).
  • Performance: Fetching all roles (management.roles.getAll()) in each login for each role can be resource-intensive if you have many roles. Consider caching role IDs or exploring if the Management API offers more direct ways to get a role ID by name if performance becomes an issue.
  • Token Size: Adding many permissions can increase the size of your tokens. Be mindful of token size limits imposed by browsers (for cookies) or HTTP headers.
  • ID Token vs. Access Token:
    • ID Token (api.idToken.setCustomClaim()): This token is primarily for the client application (e.g., your frontend). It contains information about the authenticated user, like their profile details (sub, name, email, etc.). According to the OpenID Connect (OIDC) specification, the ID Token is meant to provide identity information to the client. For client-side applications that need to make UI decisions based on user permissions (e.g., show/hide buttons, enable/disable features), having permissions directly in the ID Token is often more convenient and semantically correct. It's generally easier to parse and use on the client-side.
    • Access Token (api.accessToken.setCustomClaim()): This token is intended for resource servers (your APIs). It signifies that the bearer has been authorized to access specific resources. While Auth0 provides a toggle in the API settings ("RBAC Settings" -> "Add Permissions in the Access Token"), this only adds permissions to the Access Token.
    • Why the Auth0 Toggle Isn't Always Enough:
      • Client-Side Needs: If your client application (not just your backend API) needs to be aware of user permissions to render the UI appropriately, the Access Token permissions aren't directly helpful. The ID Token is the standard place for client-consumable identity information.
      • Server-Side Initialization & Client-Side Ease of Use: Even for server-side tasks like initializing a new user's profile or permissions in your own database upon their first login, working with the ID Token can be more straightforward. Its structure is designed for identity information. Subsequently, for client-side applications, ID Tokens are often more readily available and easier to work with in standard authentication libraries compared to Access Tokens, which might be opaque or require more effort to decode and validate for identity purposes.
      • Separation of Concerns: Keeping client-facing identity information (including permissions relevant to UI or initial user setup) in the ID Token and API-facing authorization information in the Access Token maintains a clearer separation of concerns.
    • The Action's Advantage: The Auth0 Action shown in this post specifically targets adding permissions to the ID Token. This directly addresses the need for applications (both client-side for UI and server-side for user context/initialization) to easily access and use these permissions from a token designed for identity information. While you could also add them to the Access Token using api.accessToken.setCustomClaim() within the same Action if your APIs also need them there, the primary focus here is solving the visibility and usability problem for identity-related permission data.
    Choose the appropriate token (or both) based on where the permissions will be consumed. For many UI-driven scenarios, the ID Token is the more logical and convenient place.

Alternative: Adding Roles Directly to Tokens

If your primary goal is to make the user's roles available in the tokens, and your applications can derive permissions from these roles or only need role information, a simpler Action can be used. This Action directly adds the array of role names to a custom claim.
Here's an example:
javascript
1/**
2 * Handler that will be called during the execution of a PostLogin flow.
3 *
4 * @param {Event} event - Details about the user and the context in which they are logging in.
5 * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.
6 */
7exports.onExecutePostLogin = async (event, api) => {
8 // It's good practice to use a namespace for custom claims.
9 // This namespace can be the same or different from the one used for permissions.
10 const namespace = `https://v4hub-development.infra.verify4.com`;
11
12 if (event.authorization && event.authorization.roles) {
13 // Add roles to the ID Token
14 api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
15 // Add roles to the Access Token
16 api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
17 console.log(`Successfully set roles in ID and Access tokens:`, event.authorization.roles);
18 } else {
19 console.log('No roles assigned to the user or authorization object is missing.');
20 }
21};

Key Differences and Use Cases:

  • Simplicity: This Action is much simpler as it doesn't involve calls to the Auth0 Management API.
  • Content: It adds role names (e.g., ["editor", "viewer"]) rather than a granular list of permissions (e.g., ["read:articles", "edit:articles"]).
  • Performance: This Action is more performant as it avoids external API calls.
  • Use Case: Suitable when applications are designed to interpret roles directly or when the set of permissions associated with roles is managed and understood by the client applications or APIs consuming the tokens.
You could even use both Actions if needed: one to add detailed permissions and another (or the same one extended) to add roles. The choice depends on the specific needs of your client applications and APIs.

Conclusion

Navigating Auth0 to get granular permissions into your tokens can feel like a maze, but Auth0 Actions, specifically the onExecutePostLogin trigger, offer a robust way out. By implementing the Action detailed in this post, you can finally get those much-needed permissions directly into your ID tokens. This empowers your applications to make informed authorization decisions without extra calls, streamlining your architecture.
It's a journey that, for many, has been fraught with unexpected complexity. The fact that this functionality isn't a simple toggle in the Auth0 dashboard is, frankly, baffling. However, with this Action, you now have a clear path forward. Hopefully, this solution spares you the frustration and time spent wrestling with what should be a more accessible feature, and you can get back to building great things with clear, actionable permission data in your tokens.