Last Updated: Sep 11, 2025
Overview
When using the Auth0 post-login flow to set custom claims in the token, data is received from metadata and set in the token as a custom claim. Will the custom claim be available even if the access_token is acquired via refresh_token?
Applies To
- Custom Claims
- Refresh Token
Solution
The access tokens received via a refresh token flow will usually have the same custom claims. This is because the same extensibility points (Actions) will run in this flow too. Any custom claims added to the access or ID token during the login step will also be added in a refresh token flow as long as the same Action code fully executes.
However, some use cases that require user interaction (e.g., an Auth0 Form collecting information that is then inserted as a custom claim in an Action) will not execute similarly during the refresh token flow, as this flow cannot support interactive events that require a browser. In those cases, the custom claims will be missing.
If those claims have to be retained during the refresh token call, a previously issued access or ID token with those custom claims can be passed with a custom parameter from the application side while making the refresh token call, and a special action can be used to verify and insert the claims as a workaround.
This Action must decode the received token, validate its signature, ensure that the token was issued to the user for whom the refresh token flow is running, and only then set the same custom claims in the Action. Using a token not issued to the user and not properly validating it before inserting the claims may cause a security vulnerability and requires careful implementation and testing.
Below is a sample Action that inserts the claims in an access token if the previously issued access token has those claims, and can be a good starting point. The Action assumes the access token is passed to the /oauth/token endpoint with a custom attribute named previous_access_token. This custom attribute can be named freely as long as it does not collide with a reserved name used in the flow. Best practice would be to namespace it with your company name.
exports.onExecutePostLogin = async (event, api) => {
// Import required libraries for JWT validation.
const jwt = require('jsonwebtoken');
const jwksRsa = require('jwks-rsa');
// --- 1. Handle initial login vs. Refresh Token flow ---
if (event.transaction?.protocol !== 'oauth2-refresh-token') {
// This is not a refresh token flow, so we are in an initial login.
// Set a test claim that can be retained later.
api.accessToken.setCustomClaim("https://your-app.com/claims/test", "XYZ");
return;
}
// --- 2. Check for the previous token in the request body ---
const previousToken = event.request.body.previous_access_token;
if (!previousToken) {
console.log('No previous_access_token found in refresh token request.');
return;
}
// --- 3. Decode and Validate the previous token ---
const jwksUri = `https://[login-domain]/.well-known/jwks.json`;
const jwksClient = jwksRsa({
jwksUri: jwksUri,
cache: true,
rateLimit: true,
});
const getKey = (header, callback) => {
jwksClient.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
};
let decodedToken;
try {
decodedToken = await new Promise((resolve, reject) => {
jwt.verify(
previousToken,
getKey,
{
issuer: `https://[login-domain]/`,
audience: event.authorization.audience,
algorithms: ['RS256'],
ignoreExpiration: true, // The validation doesn't check if the token has expired. Remove this line for a stricter check.
},
(err, decoded) => {
if (err) {
return reject(err);
}
resolve(decoded);
}
);
});
// --- 4. Verify that the token belongs to the current user ---
if (decodedToken.sub !== event.user.user_id) {
console.error(
`Security violation: previous_access_token subject ('${decodedToken.sub}') does not match the current user ('${event.user.user_id}').`
);
api.access.deny('Token mismatch: The provided token does not belong to the current user.');
return;
}
} catch (error) {
console.error('Validation of previous_access_token failed:', error.message);
api.access.deny('Invalid or expired previous_access_token provided.');
return;
}
// --- 5. Extract and Set the Custom Claims ---
console.log('Successfully decoded previous token payload:', JSON.stringify(decodedToken, null, 2));
const CLAIM_NAMESPACE = 'https://your-app.com/claims';
let claimsFound = false;
// --- 6. Add any additional checks specific to your use case that help to
// verify that the claims can be applied to the user. E.g. call an external service
// that holds information on users and can be cross-checked to ensure that the claims
// are safe to add.
// This is a TODO for our customers.
// Iterate over all keys in the token to find the ones that start with our namespace.
for (const key in decodedToken) {
if (key.startsWith(CLAIM_NAMESPACE)) {
claimsFound = true;
const value = decodedToken[key];
// Set the claim on the new access token using its full, original key.
api.accessToken.setCustomClaim(key, value);
console.log(`Retained claim: '${key}'`);
}
}
if (!claimsFound) {
console.log(`No claims starting with the namespace '${CLAIM_NAMESPACE}' were found in the previous token.`);
}
};