Auth0 Actions can inject arbitrary data into your tokens, but they’re fundamentally just middleware that runs before the token is issued, not a direct modification of the token after it’s been signed.
Let’s see how this looks in practice. Imagine we have a user who’s part of a "premium" group. We want to add a premium: true claim to their ID token.
/**
* Hook: post-login
*
* @param {Event} event - Details about the context of the login.
* @param {PostLoginAPI} api - Management API for PostLogin.
*/
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://myapp.example.com/';
const user = event.user;
// Check if the user has a 'premium' role (this is a simplified example)
const isPremiumUser = user.app_metadata && user.app_metadata.roles && user.app_metadata.roles.includes('premium');
if (isPremiumUser) {
api.idToken.addClaim({
[namespace + 'premium']: true,
});
api.accessToken.addClaim({
[namespace + 'premium']: true,
});
}
};
This Action runs after a user successfully logs in. If their app_metadata indicates they have the premium role, it calls api.idToken.addClaim and api.accessToken.addClaim. These methods don’t modify an existing token; they tell Auth0’s token issuance pipeline to include these new claims when it constructs the next token for this user. The namespace is crucial for custom claims to avoid conflicts with reserved JWT claims.
The core problem Auth0 Actions solve here is providing a programmatic way to enrich tokens based on dynamic user data or application logic at the time of authentication. Before Actions, this kind of customization was largely impossible or required complex workarounds like calling external APIs during the token issuance process. Now, you can directly inject data derived from user profiles, external systems (via api.access.fetch within the Action), or even based on the authentication flow itself.
The mental model to grasp is that Actions are pre-flight checks and enrichers. They intercept the authentication flow and can modify the context that Auth0 uses to build the final tokens. Think of it as a pipeline: user authenticates -> Action runs (checks permissions, fetches data, adds claims) -> Auth0 builds ID and Access tokens using the enriched context.
The event object is your window into the current authentication event. It contains user details (profile, user_metadata, app_metadata), client information, and request details. The api object is your toolkit for interacting with the Auth0 platform during the Action’s execution. You can add claims to ID and Access tokens, redirect the user, block authentication, and more.
Crucially, the api.accessToken.addClaim and api.idToken.addClaim methods don’t just take a simple key-value pair. They expect an object where keys are the claim names and values are the claim values. For custom claims, it’s best practice to prepend a unique namespace (like your application’s domain) to the claim name. This prevents collisions with standard JWT claims (like iss, sub, aud, exp) and ensures your claims are uniquely identifiable. So, instead of premium: true, you’d use https://myapp.example.com/premium: true.
When you’re debugging these Actions, remember that console.log statements within the Action code will appear in the Auth0 logs for that specific login attempt. This is invaluable for tracing the execution flow and verifying that your logic is running as expected and that the claims are being added to the event object before the token is generated.
The next hurdle you’ll likely encounter is managing the lifecycle and permissions associated with these custom claims, especially when your application needs to act upon them in different contexts.