Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AWS Cognito: Best practice to handle same user (with same email address) signing in from different identity providers (Google, Facebook)

When signing in a user with the same email address through the Google and Facebook identity providers, AWS Cognito creates multiple entries in the user pool, one entry per identity provider used:

Screenshot of AWS Cognito user pool

I have used the example code provided in this tutorial to set up AWS Cognito: The Complete Guide to User Authentication with the Amplify Framework

  • How can I create just one user instead of multiple users?
  • Is it possible to have AWS Cognito automatically combine (federate) the entries from multiple providers into one entry or should AWS Lambda functions be used to accomplish this?
like image 685
AlexR Avatar asked Jan 07 '20 20:01

AlexR


People also ask

How do I set up Facebook as a federated identity provider in an Amazon Cognito user pool?

Choose Manage User Pools. Choose an existing user pool from the list, or create a user pool. On the left navigation bar, choose Identity providers. Choose a social IdP: Facebook, Google, Login with Amazon, or Apple.

Which public identity providers can I use with Amazon Cognito identity?

You can use federation to integrate Amazon Cognito user pools with social identity providers such as Facebook, Google, and Login with Amazon. To add a social identity provider, you first create a developer account with the identity provider.

Which AWS service is an alternative for Amazon Cognito Sync for syncing of application related user data and user profile data?

If you're new to Amazon Cognito Sync, use AWS AppSync . Like Amazon Cognito Sync, AWS AppSync is a service for synchronizing application data across devices.

What does Cognito use to create unique identities and authorize users?

You can use identity pools to create unique identities for users and give them access to other AWS services.


2 Answers

Yes. You can do it by using AdminLinkProviderForUser https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html

The idea is:

  1. In PreSignUp lambda hook, we Link Provider to User if User already signed up. E.g:
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. Then when user use OAuth with Facebook/Google with User Pool, the Pool will return this User linked.

Note: You may see 2 records in User Pool UI, but when access User record detail, They already merged.

like image 147
Phan Việt Avatar answered Sep 22 '22 11:09

Phan Việt


I have been fiddling around with the same issue for a bit. Accepted answer sort of works but does not cover all scenarios. The main one is that once the user signs up with the external login, they will never be able to sign up with a username and password. Currently, Cognito does not allow linking Cognito users to external users.

My scenarios are as follows:

Scenarios

  1. When the user signs up with a username password and signs up with an external provider, link them.
  2. When the user signs up with an external provider allow them to signup with a username and password.
  3. Have a common username between all linked users to use it as a unique id in other services.

My proposed solution is to always create the Cognito user first and link all external users to it.

Proposed solution

  1. user signs up with username/password first then with an external user. No dramas, just link the external user with the Cognito user.
  2. user signs up with external user first then wants to sign up with username/password. In this scenario, create a Cognito user first then link the external user to this new Cognito user. If the user tries to signup with a username/password in the future, they will get a user already exists error. In this case, they can use the forgot password flow to recover then log in.
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 };   
like image 35
Subash Avatar answered Sep 23 '22 11:09

Subash