Issues with Linking Passwordless Users to Regular Users Using Actions

Hello Auth0 Community,

I am facing an issue with linking passwordless users (using OTP) to regular users (using username/password) through Actions. Previously, I used a Rule that worked perfectly for this purpose, but when I switched to Actions, I encountered a problem.

The Scenario

  1. A user registers with a username and password (provider: auth0).
  2. Later, the same user decides to log in using passwordless (OTP) (provider: email).
  3. Auth0 creates a new user entity for the passwordless login.
  4. My logic links this new user with the existing user based on the email address, making the original user the primary user with multiple identities (auth0 and email).

The Issue

With the Rule, everything worked as expected. The user could log in using OTP, and the identity was successfully linked on the first attempt. However, with the Action, the linking process completes, but the OTP login fails, and the user has to authenticate again for the linking to take effect.

Here are the details:

Working Rule

function (user, context, callback) {
  const request = require("request");
  const userApiUrl = auth0.baseUrl + "/users";
  const userSearchApiUrl = auth0.baseUrl + "/users-by-email";

  if (!user.email || !user.email_verified) {
    return callback(null, user, context);
  }

  request(
    {
      url: userSearchApiUrl,
      headers: {
        Authorization: "Bearer " + auth0.accessToken
      },
      qs: {
        email: user.email
      }
    },
    function(error, response, body) {
      if (error) return callback(error);
      if (response.statusCode !== 200) return callback(new Error(body));

      var data = JSON.parse(body);
      data = data.filter(function(value) {
        return value.email_verified && value.user_id !== user.user_id;
      });

      if (data.length > 1) {
        return callback(new Error("Multiple user profiles already exist - cannot select base profile"));
      }
      if (data.length === 0) {
        console.log("Skipping linking rule");
        return callback(null, user, context);
      }

      const originalUser = data[0];
      const provider = user.identities[0].provider;
      const providerUserId = user.identities[0].user_id;

      request.post(
        {
          url: userApiUrl + '/' + originalUser.user_id + '/identities',
          headers: {
            Authorization: 'Bearer ' + auth0.accessToken
          },
          json: {
            provider: provider,
            user_id: String(providerUserId)
          }
        },
        function(error, response, body) {
          if (response.statusCode >= 400) {
            return callback(new Error('Error linking account by email: ' + response.statusMessage));
          }
          context.primaryUser = originalUser.user_id;
          callback(null, user, context);
        }
      );
    }
  );
}

Migrated Action

const { ManagementClient } = require('auth0');
  
exports.onExecutePostLogin = async (event, api) => {
  if (!event.user.email || !event.user.email_verified || event.stats.logins_count > 1) return;
  try {
    const managementClient = new ManagementClient({
      domain: event.secrets.AUTH0_MGM_CLIENT_DOMAIN,
      clientId: event.secrets.AUTH0_MGM_CLIENT_ID,
      clientSecret: event.secrets.AUTH0_MGM_CLIENT_SECRET
    });

    const users = await managementClient.getUsersByEmail(event.user.email);
    
    const filteredUsers = users.filter((value) => value.email_verified && value.user_id !== event.user.user_id);
    
    if (filteredUsers.length > 1) {
      await api.access.deny("Multiple user profiles already exist - cannot select base profile");
      
      return;
    }
    if (!filteredUsers.length) return;
    
    const originalUser = filteredUsers[0];
    
    const provider = event.user.identities[0].provider;
    
    const providerUserId = event.user.identities[0].user_id;
    
    await managementClient.linkUsers(
      originalUser.user_id,
      {
        provider,
        user_id: providerUserId
      }
    );
  } catch (error) {
    console.error("Error while linking user by email", error);
    await api.access.deny("Error while linking user by email");
    
    return;
  }
};

Problem Description

When a user registers with a username/password and then attempts to log in using OTP, Auth0 creates a new user with the email provider. The linking logic is supposed to combine this new user with the existing one. The Rule successfully handled this, and the user could log in immediately with the linked identity. However, with the Action, the linking happens, but the login fails, and the user needs to authenticate again for the change to take effect.

Question

How can I achieve the same behavior with Actions as I did with Rules, where the user can log in immediately after the first attempt with OTP without requiring a second authentication attempt?

Any insights or suggestions would be greatly appreciated!

Thank you!

Found a solution.

There is a literally method exactly for this, described in docs :smiley:

api.authentication.setPrimaryUser(primary_user_id)

Change the primary user for the login transaction.

In scenarios that require linking users, the user identity used to initiate the login may no longer exist as a discrete user. That identity may now be a secondary identity of an existing user. In such situations, the setPrimaryUser() function can be used to indicate that the subject of the login should be changed.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.