Read values from previous action

Hi @chris.geihsler

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 :sweat_smile:

1 Like