Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

161
Views
AWS Cognito: mejores prácticas para manejar el mismo usuario (con la misma dirección de correo electrónico) iniciando sesión desde diferentes proveedores de identidad (Google, Facebook)

Cuando un usuario inicia sesión con la misma dirección de correo electrónico a través de los proveedores de identidad de Google y Facebook, AWS Cognito crea varias entradas en el grupo de usuarios, una entrada por proveedor de identidad utilizado:

Captura de pantalla del grupo de usuarios de AWS Cognito

He utilizado el código de ejemplo proporcionado en este tutorial para configurar AWS Cognito: la guía completa para la autenticación de usuarios con Amplify Framework

  • ¿Cómo puedo crear un solo usuario en lugar de varios usuarios?
  • ¿Es posible hacer que AWS Cognito combine (federe) automáticamente las entradas de varios proveedores en una entrada o se deben utilizar las funciones de AWS Lambda para lograrlo?
over 4 years ago · Santiago Trujillo
5 answers
Answer question

0

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

La idea es:

  1. En el gancho lambda PreSignUp, vinculamos el proveedor con el 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.

over 4 years ago · Santiago Trujillo Report

0

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

  • Si el usuario se registra con un proveedor externo, vincúlelo a cualquier cuenta existente, incluido 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 el 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, ejecutada en el activador de registro previo de Cognito

 const AWS = require("aws-sdk"); const cognito = new AWS.CognitoIdentityServiceProvider(); exports.handler = (event, context, callback) => { function checkForExistingUsers(event, linkToExistingUser) { console.log("Executing checkForExistingUsers"); var params = { UserPoolId: event.userPoolId, AttributesToGet: ['sub', 'email'], Filter: "email = \"" + event.request.userAttributes.email + "\"" }; return new Promise((resolve, reject) => cognito.listUsers(params, (err, result) => { if (err) { reject(err); return; } if (result && result.Users && result.Users[0] && result.Users[0].Username && linkToExistingUser) { console.log("Found existing users: ", result.Users); if (result.Users.length > 1){ result.Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate) ? 1 : -1); console.log("Found more than one existing users. Ordered by createdDate: ", result.Users); } linkUser(result.Users[0].Username, event).then(result => { resolve(result); }) .catch(error => { reject(err); return; }); } else { resolve(result); } }) ); } function linkUser(sub, event) { console.log("Linking user accounts with target sub: " + sub + "and event: ", event); //By default, assume the existing account is a Cognito username/password var destinationProvider = "Cognito"; var destinationSub = sub; //If the existing user is in fact an external user (Xero etc), override the the provider if (sub.includes("_")) { destinationProvider = sub.split("_")[0]; destinationSub = sub.split("_")[1]; } var params = { DestinationUser: { ProviderAttributeValue: destinationSub, ProviderName: destinationProvider }, SourceUser: { ProviderAttributeName: 'Cognito_Subject', ProviderAttributeValue: event.userName.split("_")[1], ProviderName: event.userName.split("_")[0] }, UserPoolId: event.userPoolId }; console.log("Parameters for adminLinkProviderForUser: ", params); return new Promise((resolve, reject) => cognito.adminLinkProviderForUser(params, (err, result) => { if (err) { console.log("Error encountered whilst linking users: ", err); reject(err); return; } console.log("Successfully linked users."); resolve(result); }) ); } console.log(JSON.stringify(event)); if (event.triggerSource == "PreSignUp_SignUp" || 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); }); } };
over 4 years ago · Santiago Trujillo Report

0

He estado jugando con el mismo problema por 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 un nombre de usuario y contraseña. Actualmente, Cognito no permite vincular a los usuarios de Cognito con usuarios externos.

Mis escenarios son los siguientes:

Escenarios

  1. Cuando el usuario se registre con un nombre de usuario y contraseña y se registre con un proveedor externo, vincúlelos.
  2. Cuando el usuario se registre con un proveedor externo, permita que se registre con un nombre de usuario y contraseña.
  3. Tenga un nombre de username común 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 vincular a todos los usuarios externos.

Solución propuesta

  1. el usuario se registra primero con nombre de usuario/contraseña y luego con un usuario externo. Sin dramas, simplemente vincule al usuario externo con el usuario de Cognito.
  2. el usuario se registra primero con un usuario externo y luego quiere registrarse con 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 un nombre de usuario/contraseña en el futuro, obtendrá un error de user already exists . En este caso, pueden usar el flujo de forgot password olvidada 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(async 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: '<generate random password>', Permanent: true }).promise(); return newUser.Username; }).then(username => { // link external user to cognito user const split = identity.split('_'); const providerValue = split.length > 1 ? split[1] : null; const provider = ['Google', 'Facebook'].find( val => split[0].toUpperCase() === val.toUpperCase() ); if (!provider || !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 };
over 4 years ago · Santiago Trujillo Report

0

Si desea permitir que el usuario continúe iniciando sesión con correo electrónico y contraseña (" Opción 1: el usuario se registra con nombre de usuario e inicia sesión con nombre de usuario o alias) ") además del proveedor de identidad (google, facebook, etc.), entonces la solución aceptada no No será suficiente, ya que Cognito solo puede tener un correo electrónico verificado .

Resuelvo esto agregando un activador de confirmación posterior que verifica automáticamente el correo electrónico del usuario si es necesario:

 const AWS = require('aws-sdk'); const cognitoIdp = new AWS.CognitoIdentityServiceProvider(); const markUserEmailAsVerified = async (username, userPoolId) => { console.log('marking email as verified for user with username: ' + username); const params = { UserAttributes: [ { Name: 'email_verified', Value: 'true' } // other user attributes like phone_number or email themselves, etc ], UserPoolId: userPoolId, Username: username }; const result = await new Promise((resolve, reject) => { cognitoIdp.adminUpdateUserAttributes(params, (err, data) => { if (err) { console.log( 'Failed to mark user email as verified with error:\n' + err + '\n. Manual action is required to mark user email as verified otherwise he/she cannot login with email & password' ); reject(err); return; } resolve(data); }); }); return result; }; exports.handler = async (event, context, callback) => { console.log('event data:\n' + JSON.stringify(event)); const isEmailVerified = event.request.userAttributes.email_verified; if (isEmailVerified === 'false') { await markUserEmailAsVerified(event.userName, event.userPoolId); } return callback(null, event); };

Nota : Esto no parece un desarrollo estándar o un requisito común, así que tómalo como tal.

over 4 years ago · Santiago Trujillo Report

0

En aws-sdk-js-v3 estoy usando el enfoque @subash . Descubrí que cuando realiza una devolución de llamada de error, no se crea ningún usuario adicional. Solo el que creas con tu correo electrónico.

 const { CognitoIdentityProviderClient, ListUsersCommand, AdminCreateUserCommand, AdminLinkProviderForUserCommand, AdminSetUserPasswordCommand, } = require('@aws-sdk/client-cognito-identity-provider') const client = new CognitoIdentityProviderClient({ region: process.env.REGION, }) const crypto = require("crypto") exports.handler = async(event, context, callback) => { try { const { triggerSource, userPoolId, userName, request: { userAttributes: { email, name } } } = event if (triggerSource === 'PreSignUp_ExternalProvider') { const listParam = { UserPoolId: userPoolId, Filter: `email = "${email}"`, } const listData = await client.send(new ListUsersCommand(listParam)) let [providerName, providerUserId] = userName.split('_') providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1) let linkParam = { SourceUser: { ProviderAttributeName: 'Cognito_Subject', ProviderAttributeValue: providerUserId, ProviderName: providerName, }, UserPoolId: userPoolId, } if (listData && listData.Users.length > 0) { linkParam['DestinationUser'] = { ProviderAttributeValue: listData.Users[0].Username, ProviderName: 'Cognito', } } else { const createParam = { UserPoolId: userPoolId, Username: email, MessageAction: 'SUPPRESS', UserAttributes: [{ //optional name attribute. Name: 'name', Value: name, }, { Name: 'email', Value: email, }, { Name: 'email_verified', Value: 'true', }], } const createData = await client.send(new AdminCreateUserCommand(createParam)) const pwParam = { UserPoolId: userPoolId, Username: createData.User.Username, Password: crypto.randomBytes(40).toString('hex'), Permanent: true, } await client.send(new AdminSetUserPasswordCommand(pwParam)) linkParam['DestinationUser'] = { ProviderAttributeValue: createData.User.Username, ProviderName: 'Cognito', } } await client.send(new AdminLinkProviderForUserCommand(linkParam)) //throw error to prevent additional user creation callback(Error('Social account was set, retry to sign in.'), null) } else { callback(null, event) } } catch (err) { console.error(err) } }

Sin embargo, es una mala UX ya que el primer inicio de sesión con identidad federada solo creará el usuario pero no le permitirá autenticarse. Sin embargo, el inicio de sesión posterior con identidad federada no mostrará tal problema. Avísame si obtienes alguna otra solución para ese primer inicio de sesión .

También es útil mantener email_verified como true para que el usuario pueda recuperar su contraseña. Especialmente cierto si está utilizando el autenticador aws-amplify . Esto debería estar en su activación posterior a la autenticación.

 const { CognitoIdentityProviderClient, AdminUpdateUserAttributesCommand, } = require('@aws-sdk/client-cognito-identity-provider') const client = new CognitoIdentityProviderClient({ region: process.env.REGION, }) exports.handler = async(event, context, callback) => { try { const { userPoolId, userName, request: { userAttributes: { email_verified } } } = event if (!email_verified) { const param = { UserPoolId: userPoolId, Username: userName, UserAttributes: [{ Name: 'email_verified', Value: 'true', }], } await client.send(new AdminUpdateUserAttributesCommand(param)) } callback(null, event) } catch (err) { console.error(err) } }
over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!