One scenario we have is enriching the user with data from our database and referencing it in subsequent rules, e.g.
// Enrich the idToken with seller information
const setSeller = async (user, context, callback) => {
if (!user.app_metadata.sellerId) {
return callback(null, user, context);
}
const seller = await fetchJson(`https://api.example.com/seller/${user.app_metadata.sellerId}`);
user.seller = seller;
context.idToken['https://example.com/seller'] = seller;
}
// Hash the seller email to allow us to verify the seller's identity in intercom
const hashSellerEmail = (user, context, callback) => {
if (!user.seller) {
return callback(null, user, context);
}
const hmac = crypto.createHmac(
'sha256',
configuration.intercom_verification_secret
);
hmac.update(user.seller.email);
context.idToken['https://example.com/seller_email_hash'] = hmac.digest(
'hex'
);
return callback(null, user, context);
}
And another scenario we have is providing fallback roles:
const setRoles = (user, context, callback) => {
if (!user.app_metadata.roles) {
user.app_metadata.roles = ['USER'];
}
context.accessToken['https://example.com/roles'] = roles;
return callback(null, user, context);
}
const denyRoles = (user, context, callback) => {
const allowedRoles = context.clientMetadata.allowed_roles.split(',');
// It's important, even though the default 'USER' role from `setRoles` isn't
// persisted, it can be read in this rule here.
if (!allowedRoles.some((allowedRole) => user.app_metadata.roles.includes(allowedRole))) {
throw new Error('Unauthorized');
}
callback(null, user, context);
}
Granted, in both scenarios we could get away with persisting the data to the user profile’s app_metadata in actions however in both scenarios it’d a) be a redundant API write and b) be confusing for Auth0 admin users as they may attempt to edit it and later find it’s been overridden again by a rule.
To be fair, maybe the way I’m slicing rules is too granular? I’ve always been unsure of the purpose of splitting out rules (and now actions) into separate functions…
I suppose it makes a lot of sense if you have non-critical rules which merely log to slack etc. which you may want to toggle on / off independently. However, for the core functionality I had considered just doing:
async function rule(__user, __context) {
const composeRules = rules => (user, context) => {
return rules.reduce(async (prevRule, rule) => {
const [user, context] = await prevRule;
return rule(user, context);
}, Promise.resolve(user, context));
};
// Rule 1
async function setSeller(user, context) {
return [user, context];
}
// Rule 2
async function denyRoles(user, context) {
throw new Error('Unauthorised');
return [user, context];
}
const run = composeRules([setSeller, denyRoles]);
try {
const [user, context] = await run(__user, __context);
return callback(null, user, context);
} catch (ex) {
return callback(ex);
}
}
Although I know there are limits to the rule / action size so would be gutted if we had to start writing our rule as if it was a tweet