Add Roles during Login for automatic migration

I’m currently working on migrating users from both an on-premises database and an existing Auth0 connection. To do this, I’m using automatic migration via a custom database. The reason for moving users from the existing connection is that automatic migration can only be enabled on an empty connection. To work around this, I’ve set up a new common connection and am migrating both the on-prem and existing Auth0 users into it.

In the custom database login script, when returning the user profile, I append the user’s roles (from the old connection) to app_metadata.roles_to_migrate. This is so I can assign those roles later using the Management API in a post-login action.

Unfortunately, I haven’t found a way to assign roles directly within the custom database script during user creation. If there’s a better pattern for this, I’d love to hear it.

The Post-Login Role Assignment

In the post-login action, I use a helper function assignMigratedRoles to:

  1. Check if app_metadata.migrate_roles === true.
  2. If so, use the Management API to assign the roles listed in roles_to_migrate.
  3. Clear migrate_roles and roles_to_migrate from app_metadata.

However, because we’re using RBAC, the permissions associated with these roles need to be included in the access token. This doesn’t happen on the first login, since the role assignment occurs after the access token is generated.

Current Workaround (Hack)

As a hack, I trigger the post-login actions again by calling:

api.user.setAppMetadata('force_retry', null);

…from within the role assignment function — even though api isn’t passed to it. Somehow this causes the action to re-run, and the next login includes the correct permissions. Clearly, this is not a reliable or clean solution.

Here’s the relevant code for reference:

exports.onExecutePostLogin = async (event, api) => {
  if (event.connection.name === 'Primary') {
    await assignMigratedRoles(event, api);
  }
};

const assignMigratedRoles = async (event, api) => {
  const {
    MANAGEMENT_CLIENT_ID,
    MANAGEMENT_CLIENT_SECRET,
    MANAGEMENT_CLIENT_DOMAIN,
  } = event.secrets;

  if (event.user.app_metadata.migrate_roles === true) {
    const rolesToMigrate = event.user.app_metadata.roles_to_migrate;

    const client = new ManagementClient({
      clientId: MANAGEMENT_CLIENT_ID,
      clientSecret: MANAGEMENT_CLIENT_SECRET,
      domain: MANAGEMENT_CLIENT_DOMAIN,
    });

    const allRoles = await client.getRoles();

    const roleIdsToAssign = allRoles
      .filter((role) => rolesToMigrate.includes(role.name))
      .map((role) => role.id);

    if (roleIdsToAssign.length > 0) {
      await Promise.all([
        client.assignRolestoUser(
          { id: event.user.user_id },
          { roles: roleIdsToAssign },
        ),
        client.updateAppMetadata(
          { id: event.user.user_id },
          {
            ...event.user.app_metadata,
            roles_to_migrate: null,
            migrate_roles: null,
          },
        ),
      ]);

      // Hack to trigger the post-login action again
      api.user.setAppMetadata('force_retry', null);
    }
  }
};

Questions

  1. How can I assign roles during login in such a way that the associated permissions are included in the access token right away?
  2. Is there a better approach to assign roles from within the custom database login script? For example, can I directly assign roles during user creation or call out to the Management API from that script?

Hi @andreas.h

Thank you for providing all that information regarding the matter!

How can I assign roles during login in such a way that the associated permissions are included in the access token right away?

Unfortunately, as you have mentioned, when assigning a role to a newly created user, these roles and permissions will not be visible during the first time they authenticate due to the access token being generated after these are assigned to the user.

The hack you have presented would be a suitable way in order to force these attributes to be available in the access token on the first login until all the users are migrated. Otherwise, you could also perform a silent authentication within your application so that you do not have to force the action to be ran twice, but have the user logged in again and returning a proper token.

Is there a better approach to assign roles from within the custom database login script? For example, can I directly assign roles during user creation or call out to the Management API from that script?

If you would be trying to assign roles to the migrated user during the Custom DB script, the results would be the same. You would need to force re-authentication of the migrated user so that the access token can include the roles and permissions assigned to them.

If I can help you with any other questions, let me know!

Kind Regards,
Nik

Okey. I’m fine with the hack if I understand why it works. I’ve adjust the code now to throw an error instead since I guess that was what was going on when trying to call an undefined object. So after I have updated the role of the user I throw an error and it seems to work. Is this because the actions retry a couple of time and in the next attempt the role has been assign?

Roles and RBAC permissions is part of the first login with the code below:

    if (roleIdsToAssign.length > 0) {
      await Promise.all([
        client.assignRolestoUser(
          { id: event.user.user_id },
          { roles: roleIdsToAssign },
        ),
        client.updateAppMetadata(
          { id: event.user.user_id },
          {
            ...event.user.app_metadata,
            roles_to_migrate: null,
            migrate_roles: null,
          },
        ),
      ]);

      throw('retry');
    }

As previously with the api object, without throw('retry'); the roles and permissions are not present.

Yes, that might be the case. It can be quite tricky to assign roles to new.migrated users due to the fact that the token is generated after the Management API does its tricks. I understand that it can be quite wacky to force re-authentication or silent authentication is these cases but your hack looks great to force these attributes to be assigned during the mgiration!

Kind Regards,
Nik