Read values from previous action

With rules, we were able to effectively do this:

rule3(rule2(rule1(user, context, callback)));

Meaning subsequent rules could read properties added to the user / access_token / id_token etc. by a previous rule. e.g.

function rule1(user, context, callback) {
  user.a = 'foo';
  user.app_metadata.b = 'bar';
  context.idToken['https://example.com/c'] = 'baz';
  context.accessToken['https://example.com/d'] = 'qux';
  return callback(null, user, context);
}

function rule2(user, context, callback) {
  console.log(user.a); // 'foo'
  console.log(user.app_metadata.b); // 'bar'
  console.log(context.idToken['https://example.com/c']); // 'baz'
  console.log(context.accessToken['https://example.com/d']); // 'qux'
  return callback(null, user, context);
}

Is this possible with actions?

Interestingly enough, I’ve noticed that mutating user.app_metadata in an action actually sets it on the users profile – i.e it makes an API call to update the user DB; this is quite unexpected e.g.

exports.onExecutePostLogin = async (event, api) => {
    event.user.app_metadata.a = 'foo'; // Updates the user profile
    api.user.setAppMetadata('b', 'bar'); // So what's the point of this?
};

Thanks for the feedback @riscarrott! As for the question about mutating app_metadata, that is not intended, and is a bug that we’ll fix soon. Sorry about that!

A design goal with Actions is that we’d like to keep the event object immutable and capture all side effects in the api object. So, the fact that app_metadata can be updated like this directly is a miss on our part.

As for communicating across actions, that is not currently possible apart from metadata updates made by previous Actions, although we’ve heard the need for this in a more general sense. We’ll address it in the future, but we’re still working out the best way to expose this in Actions.

Would you mind sharing a little bit more about your use case for sharing data across actions like this? What sorts of things are trying to do that aren’t possible if you can’t see claims, etc. added by previous Actions?

Thanks again for your question and sharing your experience with us!

2 Likes

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: