Saltar al contenido

AWS Cognito: práctica recomendada para gestionar el inicio de sesión del mismo usuario (con la misma dirección de correo electrónico) desde diferentes proveedores de identidad (Google, Facebook)

Nuestro equipo redactor ha estado mucho tiempo buscando la respuesta a tu pregunta, te brindamos la solución así que nuestro deseo es que sea de gran ayuda.

Solución:

Si. Puedes hacerlo usando AdminLinkProviderForUser https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html

La idea es:

  1. En el gancho lambda de PreSignUp, vinculamos el proveedor al usuario si el usuario ya se registró. P.ej:
import CognitoIdentityServiceProvider from 'aws-sdk/clients/cognitoidentityserviceprovider'

const cognitoIdp = new CognitoIdentityServiceProvider()
const getUserByEmail = async (userPoolId, email) => 
 const params = 
   UserPoolId: userPoolId,
   Filter: `email = "$email"`
 
 return cognitoIdp.listUsers(params).promise()


const linkProviderToUser = async (username, userPoolId, providerName, providerUserId) => 
 const params = 
   DestinationUser: 
     ProviderAttributeValue: username,
     ProviderName: 'Cognito'
   ,
   SourceUser: 
     ProviderAttributeName: 'Cognito_Subject',
     ProviderAttributeValue: providerUserId,
     ProviderName: providerName
   ,
   UserPoolId: userPoolId
 

 const result = await (new Promise((resolve, reject) => 
   cognitoIdp.adminLinkProviderForUser(params, (err, data) => 
     if (err) 
       reject(err)
       return
     
     resolve(data)
   )
 ))

 return result


exports.handler = async (event, context, callback) => 
 if (event.triggerSource === 'PreSignUp_ExternalProvider') 
   const userRs = await getUserByEmail(event.userPoolId, event.request.userAttributes.email)
   if (userRs && userRs.Users.length > 0) 
     const [ providerName, providerUserId ] = event.userName.split('_') // event userName example: "Facebook_12324325436"
     await linkProviderToUser(userRs.Users[0].Username, event.userPoolId, providerName, providerUserId)
    else 
     console.log('user not found, skip.')
   

 
 return callback(null, event)

  1. Luego, cuando el usuario use OAuth con Facebook / Google con el grupo de usuarios, el grupo devolverá este usuario vinculado.

Nota: Es posible que vea 2 registros en la interfaz de usuario del grupo de usuarios, pero cuando acceda a los detalles del registro de usuario, ya se fusionaron.

He estado jugando con el mismo problema durante un tiempo. La respuesta aceptada funciona, pero no cubre todos los escenarios. La principal es que una vez que el usuario se registra con el inicio de sesión externo, nunca podrá registrarse con nombre de usuario y contraseña. Actualmente, Cognito no permite vincular un usuario de Cognito a un usuario externo.

Mis escenarios son los siguientes:

Escenarios

  1. Cuando el usuario se registre con la contraseña del nombre de usuario y se registre con un proveedor externo, vincúlelos.
  2. Cuando el usuario se registra con un proveedor externo, permítale registrarse con nombre de usuario y contraseña.
  3. Tener un comun username entre todos los usuarios vinculados para usarlo como una identificación única en otros servicios.

Mi solución propuesta es siempre crear primero el usuario de Cognito y vincularlo a todos los usuarios externos.

Solución propuesta

  1. el usuario se registra primero con un nombre de usuario / contraseña y luego con un usuario externo. Sin dramas, solo vincula al usuario externo con el usuario de Cognito.
  2. el usuario se registra primero con un usuario externo y luego desea registrarse con un nombre de usuario / contraseña. En este escenario, primero cree un usuario de Cognito y luego vincule el usuario externo a este nuevo usuario de Cognito. Si el usuario intenta registrarse con nombre de usuario / contraseña en el futuro, obtendrá user already exists error. En este caso, pueden usar forgot password fluir para recuperarse y luego iniciar sesión.
const 
  CognitoIdentityServiceProvider
 = require('aws-sdk');


const handler = async event => 
  const userPoolId = event.userPoolId;
  const trigger = event.triggerSource;
  const email = event.request.userAttributes.email;
  const givenName = event.request.userAttributes.given_name;
  const familyName = event.request.userAttributes.family_name;
  const emailVerified = event.request.userAttributes.email_verified;
  const identity = event.userName;
  const client = new CognitoIdentityServiceProvider();

  if (trigger === 'PreSignUp_ExternalProvider') 

    await client.listUsers(
        UserPoolId: userPoolId,
        AttributesToGet: ['email', 'family_name', 'given_name'],
        Filter: `email = "$email"`
      )
      .promise()
      .then((
        Users
      ) => Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate ? 1 : -1)))
      .then(users => users.length > 0 ? users[0] : null)
      .then(user => 
        // user with username password already exists, do nothing
        if (user) 
          return user;
        

        // user with username password does not exists, create one
        const newUser = await client.adminCreateUser(
            UserPoolId: userPoolId,
            Username: email,
            MessageAction: 'SUPPRESS', // dont send email to user
            UserAttributes: [
                Name: 'given_name',
                Value: givenName
              ,
              
                Name: 'family_name',
                Value: familyName
              ,
              
                Name: 'email',
                Value: email
              ,
              
                Name: 'email_verified',
                Value: emailVerified
              
            ]
          )
          .promise();
          // gotta set the password, else user wont be able to reset it
          await client.adminSetUserPassword(
              UserPoolId: userPoolId,
              Username: newUser.Username,                                                      
              Password: '',                                                       
              Permanent: true
          ).promise();
    
          return newUser.Username;
      ).then(username =>  !providerValue) 
          return Promise.reject(new Error('Invalid external user'));
        

        return client.adminLinkProviderForUser(
            UserPoolId: userPoolId,
            DestinationUser: 
              ProviderName: 'Cognito',
              ProviderAttributeValue: username
            ,
            SourceUser: 
              ProviderName: provider,
              ProviderAttributeName: 'Cognito_Subject',
              ProviderAttributeValue: providerValue
            
          )
          .promise()
      );
  

  return event;
;

module.exports = 
  handler
;


La solución que creé maneja, creo, todos los casos. También aborda algunos problemas comunes con Cognito.

  • Si el usuario se está registrando con un proveedor externo, vincúlelo a cualquier cuenta existente, incluyendo Cognito (nombre de usuario / contraseña) o cuenta de proveedor externo.
  • Cuando se vincule a cuentas existentes, vincule solo a la cuenta más antigua. Esto es importante si tiene más de 2 opciones de inicio de sesión.
  • Si el usuario se está registrando con Cognito (nombre de usuario / contraseña), si ya existe un proveedor externo, rechace el registro con un mensaje de error personalizado (porque las cuentas no se pueden vincular).

Tenga en cuenta que al vincular cuentas, el activador de registro previo de Cognito devuelve un error “Ya se encontró una entrada para el nombre de usuario”. Su cliente debe manejar esto y volver a intentar la autenticación, o pedirle al usuario que inicie sesión nuevamente. Más información sobre esto aquí:

El flujo de autenticación de Cognito falla con “Ya se encontró una entrada para el nombre de usuario Facebook_10155611263153532”

Aquí está mi lambda, ejecutado en el disparador de registro previo de Cognito

const AWS = require("aws-sdk");
const cognito = new AWS.CognitoIdentityServiceProvider();

exports.handler = (event, context, callback) =>  event.triggerSource == "PreSignUp_AdminCreateUser") 

    checkForExistingUsers(event, false).then(result => 
        if (result != null && result.Users != null && result.Users[0] != null) 
          console.log("Found at least one existing account with that email address: ", result);
          console.log("Rejecting sign-up");
          //prevent sign-up
          callback("An external provider account alreadys exists for that email address", null);
         else 
          //proceed with sign-up
          callback(null, event);
        
      )
      .catch(error => 
        console.log("Error checking for existing users: ", error);
        //proceed with sign-up
        callback(null, event);
      );

  

  if (event.triggerSource == "PreSignUp_ExternalProvider") 

    checkForExistingUsers(event, true).then(result => 
        console.log("Completed looking up users and linking them: ", result);
        callback(null, event);
      )
      .catch(error => 
        console.log("Error checking for existing users: ", error);
        //proceed with sign-up
        callback(null, event);
      );

  

;

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *