Need Help Enforcing Email Uniqueness Across Social and Database Connections

Hi Auth0 Community,

I’m having trouble ensuring email uniqueness across social and database connections on my Auth0 setup. I need to prevent users from registering or logging in with a social connection (like Google) if their email is already used in a Username-Password database connection, and vice versa.

Scenario:

  1. Setup:
  • Node.js + Typescript web platform.
  • Auth0 for authentication.
  • Users can register with both Username-Password and Social Connections.
  1. Requirements:
  • Email Uniqueness: Email must be unique across all connection types.
  • Block Social Login if Email Exists in Database Connection: If a user tries to log in with a social connection and their email is already registered in a Username-Password database connection, they should be blocked.
  • Block Database Registration if Email Exists in Social Connection: If a user tries to register with a Username-Password database connection and their email is already registered via a social connection, they should be blocked.
  1. Current Implementation:
  • Pre-User Registration Action: Checks for existing emails before completing registration with a Username-Password database connection.
  • Post-Login Action: Intended to check if an email exists in any connection type when a user logs in with a social connection.
  1. Problems Encountered:
  • Using the Auth0 Management API in actions to fetch users by email.
  • Handling API tokens within Auth0 Actions.
  • Consistently getting errors (Extensibility error in all LOGIN/SINGUP when I deploy the Actions) and unable to achieve the desired email uniqueness enforcement.
  1. Code Snippets:

Pre-User Registration Action:

  • I have the respective Secrets and Dependencies configured.
  • The function is doing 2 things: preventing repeated emails for being registered within Auth0 & having an encrypted connection with my database to store the user information.
const axios = require("axios");
const crypto = require("crypto");

function encryptToken(token, key, iv) {
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
  let encrypted = cipher.update(token, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return iv.toString('hex') + ':' + encrypted; // Include the IV with the encrypted data
}

exports.onExecutePreUserRegistration = async (event, api) => {
  const token = event.secrets.SECRET;
  const key = Buffer.from(event.secrets.ENCRYPTION_KEY, 'hex');
  const iv = crypto.randomBytes(16);

  const encryptedToken = encryptToken(token, key, iv);

  const userData = {
    email: event.user.email,
    ip: event.request.ip,
    country: event.request.geoip.countryName,
    city: event.request.geoip.cityName,
    image: event.user.picture,
  };

  const config = {
    headers: {
      'Authorization': `Bearer ${encryptedToken}`
    }
  };

  const email = event.user.email;

  const ManagementClient = require('auth0').ManagementClient;

  const management = new ManagementClient({
      domain: event.secrets.domain,
      clientId: event.secrets.clientId,
      clientSecret: event.secrets.clientSecret,
  });

  try {
    const res = await management.usersByEmail.getByEmail(email);

    if (res.data.length > 0) {
      // Email already exists
      api.access.deny("email already used", "email already used");
    }

    const response = await axios.post('https://backend.fanz.com.ar/public/users', userData, config);
    console.log('Data sent to server:', response.data);
  } catch (err) {
    console.error('Failed to send data to server:', err);
    api.access.deny("email already used","email already used");
    throw new Error('Failed to communicate with server');
  }
};

Post-Login Action:

  • Dependencies and Secrets are correct.
const axios = require('axios');
const crypto = require('crypto');
const { ManagementClient } = require('auth0');

function encryptToken(token, key, iv) {
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
  let encrypted = cipher.update(token, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return iv.toString('hex') + ':' + encrypted; // Include the IV with the encrypted data
}

exports.onExecutePostLogin = async (event, api) => {
  if (event.connection.strategy === 'auth0') {
    // If the user is logging in with a database connection, we don't need to check anything.
    return;
  }

  const token = event.secrets.SECRET;
  const key = Buffer.from(event.secrets.ENCRYPTION_KEY, 'hex');
  const iv = crypto.randomBytes(16);

  const encryptedToken = encryptToken(token, key, iv);

  const userData = {
    email: event.user.email,
    ip: event.request.ip,
    country: event.request.geoip.countryName,
    city: event.request.geoip.cityName,
    image: event.user.picture,
  };

  const config = {
    headers: {
      'Authorization': `Bearer ${encryptedToken}`
    }
  };

  const email = event.user.email;

  const management = new ManagementClient({
    domain: event.secrets.domain,
    clientId: event.secrets.clientId,
    clientSecret: event.secrets.clientSecret,
  });

  try {
    const users = await management.getUsersByEmail(email);

    const userExistsInDatabase = users.some(user => 
      user.identities.some(identity => identity.provider === 'auth0')
    );

    if (userExistsInDatabase) {
      // User exists with database connection
      api.access.deny("email already used", "email already used");
    }

    // Continue with logging in the user
    const response = await axios.post('https://backend.fanz.com.ar/public/users', userData, config);
    console.log('Data sent to server:', response.data);
  } catch (err) {
    console.error('Failed to send data to server:', err);
    api.access.deny("email already used", "email already used");
    throw new Error('Failed to communicate with server');
  }
};

Any help or guidance would be greatly appreciated!

Thanks in advance,
Julian

Pd: Tried this Action for Pre User Registration but didn’t work

It could be worth taking a step back to think about the limitations and quirks of email addresses and the management API, and then define your own expectations.

Matching email addresses across the tenant can be difficult because:

  1. /users-by-email is case-sensitive. (And alternative search functions are only eventually consistent.) [1]
  2. Custom database email addresses are stored in lower-case in Auth0.
  3. But social email addresses preserve the case of the federated provider (they may follow the RFCs which technically make email addresses case-sensitive).

This can lead to a scenario like:

  1. Alice@example.com registers using a federated provider that preserves case.
  2. alice@example.com begins to register using a custom database (lower-cased).
  3. The action searches getUsersByEmail("alice@example.com") which fails to locate Alice@example.com.
  4. If your resource server intended to treat Alice and alice as truly unique users (unlikely), then this is OK. But if they should be treated the same, alice will also complete signup/login and there will be unexpected duplicated users.

This can also unfold in the other direction: first register a lower-cased custom database user email, then a mixed-case federated provider user email. But in that situation, it might be a mitigation to lower-case the event email address before searching. However, if you ever intend to have two or more federated providers excluding each other, lower-casing the event email address may not be enough.

As an alternative approach, given there is already some HTTP request to your backend /public/users endpoint, consider using that as a source of truth for registered email addresses as your application sees it. Your application can choose its own interpretation of email address normalization like lower-case always, or preserve but lower-case on compare only, or truly distinguish case, and/or whether to deny or collapse or separate plus addresses (alice+test@example.com), and/or internationalization handling, etc. So in the post-login action: optimistically submit the user to your backend; then only if the backend accepts the submission and responds successfully, continue post-login, else deny.