Passer au contenu principal

La disponibilité varie selon le forfait Auth0

La disponibilité de cette fonctionnalité dépend à la fois de votre implémentation de connexion et de votre forfait Auth0 ou de votre entente personnalisée. Pour en savoir plus, consultez Tarification.
Vous pouvez lier des comptes d’utilisateur de différentes façons :
  • Action avec une application externe de liaison de comptes
  • Auth0
  • bibliothèque Auth0.js

Action avec une application externe de liaison de comptes

Vous pouvez utiliser une Action avec une application externe de liaison de comptes pour lier des comptes d’utilisateur à l’aide de la Management API.
Auth0 ne bascule pas automatiquement vers le bon utilisateur principal après la liaison de comptes; ce changement doit donc être effectué dans le code des Actions une fois la liaison de comptes réussie.Chaque liaison manuelle de comptes doit inviter l’utilisateur à saisir ses identifiants. Votre locataire doit exiger l’authentification des deux comptes avant la liaison afin d’éviter que des acteurs malveillants accèdent à des comptes d’utilisateur légitimes.
Les étapes suivantes illustrent un exemple d’implémentation :
  1. L’Action identifie les comptes d’utilisateur potentiels à lier (s’ils existent).
  2. L’Action redirige l’utilisateur vers une application externe de liaison de comptes avec un payload de jeton contenant les identités utilisateur candidates :
    {
      "current_identity": {
        "user_id": event.user.user_id,
        "provider": event.connection.strategy,
        "connection": event.connection.name
      },
      "candidate_identities": [
        {
          "user_id": USER_ID_1,
          "provider": PROVIDER_1,
          "connection": CONNECTION_1
        },
        {
          "user_id": USER_ID_2,
          "provider": PROVIDER_2,
          "connection": CONNECTION_2
        },
        ...
      ]
    }
    
  3. L’application externe de liaison de comptes invite l’utilisateur à s’authentifier à l’aide des identifiants du compte qu’il souhaite lier.
  4. L’application externe de liaison de comptes redirige l’utilisateur vers l’Action avec un payload de jeton contenant les identités utilisateur principale et secondaire :
    {
      "primary_identity": {
        "user_id": PRIMARY_USER_ID,
        "provider": PRIMARY_PROVIDER_STRATEGY,
        "connection": PRIMARY_CONNECTION_NAME,
      },
      "secondary_identity": {
        "user_id": SECONDARY_USER_ID,
        "provider": SECONDARY_PROVIDER_STRATEGY,
        "connection": SECONDARY_CONNECTION_NAME,
      }
    }
    
  5. L’Action valide l’authenticité et le contenu du jeton.
  6. L’Action appelle la Management API pour lier les comptes en fonction des résultats de l’application externe de liaison de comptes.
  7. L’Action bascule vers l’utilisateur principal s’il ne correspond pas à event.user.user_id.

Exemple : Action de liaison de comptes

const { ManagementClient, AuthenticationClient } = require('auth0');

/**
 * Action de liaison de comptes Auth0 - Version de production
 *
 * Dépendance requise : auth0@5.3.1
 *
 * Cette Action détecte les utilisateurs ayant des comptes en double (même courriel vérifié)
 * et les redirige vers un service externe pour gérer la liaison de comptes.
 */

const ACCOUNT_LINKING_TIMESTAMP_KEY = 'account_linking_timestamp';
const TTL_LEEWAY_FACTOR = 0.2;
const PROPERTIES_TO_COMPLETE = ['given_name', 'family_name', 'name'];

/**
 * Obtenir le jeton d'accès à la Management API avec mise en cache
 */
const getManagementAccessToken = async (event, api) => {
  const cacheKey = `mgmt-api-token-${event.secrets.MANAGEMENT_API_CLIENT_ID}`;
  const cached = api.cache.get(cacheKey);

  if (cached && cached.value) {
    return cached.value;
  }

  const auth = new AuthenticationClient({
    domain: event.secrets.MANAGEMENT_API_DOMAIN,
    clientId: event.secrets.MANAGEMENT_API_CLIENT_ID,
    clientSecret: event.secrets.MANAGEMENT_API_CLIENT_SECRET
  });

  const response = await auth.oauth.clientCredentialsGrant({
    audience: `https://${event.secrets.MANAGEMENT_API_DOMAIN}/api/v2/`
  });

  
  const accessToken = response.access_token || response.data?.access_token;
  const expiresIn = response.expires_in || response.data?.expires_in;

  if (accessToken && typeof accessToken === 'string') {
    api.cache.set(cacheKey, accessToken, {
      ttl: expiresIn - expiresIn * TTL_LEEWAY_FACTOR
    });
  }

  return accessToken;
};

/**
 * Obtenir les utilisateurs ayant la même adresse de courriel vérifiée
 */
const getUsersWithSameEmail = async (event, api) => {
  const accessToken = await getManagementAccessToken(event, api);
  const management = new ManagementClient({
    domain: event.secrets.MANAGEMENT_API_DOMAIN,
    token: accessToken
  });

  
  const users = await management.users.listUsersByEmail({
    email: event.user.email
  });

  return users;
};

/**
 * Filtrer et mapper les identités candidates avec courriel vérifié
 */
const getCandidateIdentitiesWithVerifiedEmail = (event, candidateUsers) => {
  return candidateUsers
    .filter((user) => user.user_id !== event.user.user_id && user.email_verified === true)
    .filter((user) => user.identities && user.identities.length > 0)
    .map((user) => ({
      user_id: user.user_id,
      provider: user.identities[0].provider,
      connection: user.identities[0].connection
    }));
};

/**
 * Lier les comptes à l'aide de la Management API
 * Lie l'identité secondaire À l'identité principale
 */
const linkAccounts = async (event, primaryIdentity, secondaryIdentity) => {
  const accessToken = await getManagementAccessToken(event, { cache: { get: () => null, set: () => {} } });

  // Extraire la partie de l'identifiant après le | pour l'API
  const idParts = secondaryIdentity.user_id.split('|');
  const userId = idParts.length > 1 ? idParts[1] : secondaryIdentity.user_id;

  const url = `https://${event.secrets.MANAGEMENT_API_DOMAIN}/api/v2/users/${encodeURIComponent(primaryIdentity.user_id)}/identities`;

  const body = {
    provider: secondaryIdentity.provider,
    user_id: userId
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Link API error: ${response.status} - ${errorText}`);
  }

  return await response.json();
};

/**
 * Compléter les propriétés de profil manquantes à partir des identités liées
 */
const completeProperties = (event, api) => {
  for (const property of PROPERTIES_TO_COMPLETE) {
    if (!event.user[property]) {
      for (const identity of event.user.identities) {
        if (identity.profileData && identity.profileData[property]) {
          api.idToken.setCustomClaim(property, identity.profileData[property]);
          break;
        }
      }
    }
  }
};

/**
 * onExecutePostLogin - Détecter les comptes en double et rediriger vers le service de liaison
 */
exports.onExecutePostLogin = async (event, api) => {
  // Valider la configuration
  if (
    !event.secrets.MANAGEMENT_API_DOMAIN ||
    !event.secrets.MANAGEMENT_API_CLIENT_ID ||
    !event.secrets.MANAGEMENT_API_CLIENT_SECRET ||
    !event.secrets.SESSION_TOKEN_SHARED_SECRET ||
    !event.secrets.ACCOUNT_LINKING_SERVICE_URL
  ) {
    console.log('Configuration requise manquante - liaison de comptes ignorée');
    return;
  }

// Nous ne traiterons pas les utilisateurs pour la liaison de comptes tant qu'ils n'ont pas vérifié leur adresse de courriel.
  // Nous pourrions envisager de rejeter les connexions ici ou de rediriger les utilisateurs vers un outil externe pour
  // rappeler à l'utilisateur de confirmer son adresse de courriel avant de continuer.
  //
  // Dans cet exemple, nous ne traiterons simplement pas les utilisateurs à moins que leur courriel ne soit vérifié.
  if (!event.user.email_verified) {
    return;
  }

  // Ignorer si déjà traité
  if (event.user.app_metadata && event.user.app_metadata[ACCOUNT_LINKING_TIMESTAMP_KEY]) {
    completeProperties(event, api);
    return;
  }

  try {
    // Trouver les utilisateurs ayant le même courriel
    const candidateUsers = await getUsersWithSameEmail(event, api);

    if (!Array.isArray(candidateUsers) || candidateUsers.length === 0) {
      return;
    }

    // Filtrer pour les courriels vérifiés
    const candidateIdentities = getCandidateIdentitiesWithVerifiedEmail(event, candidateUsers);

    if (candidateIdentities.length === 0) {
      return;
    }

    // Créer le jeton de session
    const sessionToken = api.redirect.encodeToken({
      payload: {
        current_identity: {
          user_id: event.user.user_id,
          provider: event.connection.strategy,
          connection: event.connection.name
        },
        candidate_identities: candidateIdentities,
        email: event.user.email,
        continue_url: `https://${event.request.hostname}/continue`
      },
      secret: event.secrets.SESSION_TOKEN_SHARED_SECRET,
      expiresInSeconds: 120
    });

    // Rediriger vers le service de liaison
    api.redirect.sendUserTo(event.secrets.ACCOUNT_LINKING_SERVICE_URL, {
      query: {
        session_token: sessionToken
      }
    });

  } catch (err) {
    console.error('Erreur de liaison de comptes :', err.message);
    // Ne pas bloquer la connexion en cas d'erreur
  }
};

/**
 * onContinuePostLogin - Traiter la décision de liaison de l'utilisateur
 */
exports.onContinuePostLogin = async (event, api) => {
  try {
    // Valider le jeton de réponse
    const { primary_identity: primaryIdentity, secondary_identity: secondaryIdentity } = api.redirect.validateToken({
      secret: event.secrets.SESSION_TOKEN_SHARED_SECRET,
      tokenParameterName: 'session_token'
    });

    if (!primaryIdentity || !secondaryIdentity) {
      // L'utilisateur a annulé - continuer sans liaison
      return;
    }

    const currentUserId = event.user.user_id;

    // CRITIQUE : Basculer vers l'utilisateur principal AVANT la liaison
    // Cela évite l'erreur « Unable to construct login user »
    if (primaryIdentity.user_id !== currentUserId) {
      api.authentication.setPrimaryUser(primaryIdentity.user_id);
    }

    // Lier le compte secondaire
    const linkedIdentities = await linkAccounts(event, primaryIdentity, secondaryIdentity);

    if (linkedIdentities && linkedIdentities.length > 0) {
      // Marquer comme traité
      api.user.setAppMetadata(ACCOUNT_LINKING_TIMESTAMP_KEY, Date.now());
      completeProperties(event, api);
    } else {
      api.access.deny('Échec de la liaison de comptes');
    }
  } catch (err) {
    console.error('Erreur onContinuePostLogin :', err.message);
    api.access.deny('Erreur de liaison de comptes : ' + err.message);
  }
};

Management API

Vous pouvez utiliser le point de terminaison Lier un compte utilisateur de la Management API de deux façons :
  • Liaison de comptes côté client initiée par l’utilisateur à l’aide de avec le scope update:current_user_identities.
  • Liaison de comptes côté serveur à l’aide de jetons d’accès avec le scope update:users.

Liaison de comptes côté client initiée par l’utilisateur

Pour effectuer une liaison de comptes côté client initiée par l’utilisateur, vous avez besoin d’un jeton d’accès contenant les éléments suivants dans le payload :
  • update:current_user_identites scope
  • le user_id du compte principal dans l’URL
  • du compte secondaire, signé avec RS256 et comprenant une revendication aud qui identifie l’application correspondant à la valeur de la revendication azp du jeton d’accès demandeur.
Un jeton d’accès qui contient le scope update:current_user_identities ne peut être utilisé que pour mettre à jour les renseignements de l’utilisateur actuellement connecté. Cette méthode convient donc aux scénarios où l’utilisateur initie le processus de liaison.

Liaison de comptes côté serveur

Pour effectuer une liaison de comptes côté serveur, vous avez besoin d’un jeton d’accès qui contient les éléments suivants dans le payload :
  • scope update:users
  • user_id du compte principal dans l’URL
  • user_id du compte secondaire
  • jeton d’identité du compte secondaire, signé avec RS256, qui inclut une revendication aud identifiant l’application correspondant à la valeur de la revendication azp du jeton d’accès utilisé pour la requête.
Les jetons d’accès qui contiennent le scope update:users peuvent servir à mettre à jour les informations de n’importe quel utilisateur. Par conséquent, cette méthode est destinée uniquement au code côté serveur. Le user_id et le provider du compte secondaire peuvent être déduits à partir de son identifiant unique. Par exemple, pour l’identifiant google-oauth2|108091299999329986433 :
  • provider est google-oauth2
  • user_id est 108091299999329986433
Vous pouvez aussi envoyer le jeton d’identité du compte secondaire au lieu du provider et du user_id :

Bibliothèque Auth0.js

Vous pouvez utiliser la bibliothèque Auth0.js pour effectuer une liaison de comptes côté client. Consultez Référence Auth0.js v9 > Gestion des utilisateurs pour en savoir plus.

En savoir plus