3

Currently Cognito allows merging federated users (users logging from external identity providers like Google) to native users (users who signed up via username and password combination).

Is there a way to merge an existing federated user with a new native user?

2 Answers2

6

No. You can only create a link from a user who has never signed in. That is, you can only link the new user at the point they are created (in the pre-auth trigger).

"This allows you to create a link from the existing user account to an external federated user identity that has not yet been used to sign in".

Personally I catch this case in the pre-signup trigger, and reject the sign up with a custom message ("An account already exists with this email address, please sign in using Google").

Here is my pre-signup lambda in case you find it useful

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);
      });

  }

};
F_SO_K
  • 13,640
  • 5
  • 54
  • 83
0

Here is the solution: Add below lambda code as a pre-sign up Cognito trigger.

On every Federated login, if a native user exists, then just link the federated user to the native user. If a native user does not exist, then create the native user first, and then link. When creating the native user from Lambda trigger, be careful, as it can lead to recursion of user creation and their trigger calls. Hence stopping the recursion is very important, as handled below:

import boto3
client = boto3.client('cognito-idp')
def lambda_handler(event, context):
    print("Event: ", event)
    if event['triggerSource'] == 'PreSignUp_AdminCreateUser':
        print('STOP RECURSION') 
        return event #Creating a user from lambda trigger, can further trigger a chain of user creation and their triggers, hence stopping recursion for admin create used below
        
    email = event['request']['userAttributes']['email']

    response = findExistingUsersByEmail(event, email)
    
    print('Native user search result: ', response['Users'])
    
    if not response['Users']:
        print('Creating native user for email: ', email)
        signup_response = client.admin_create_user(
                    UserPoolId=event["userPoolId"],
                    Username=email,
                    UserAttributes=[
                        {
                            'Name': 'email',
                            'Value': email
                        },
                    ],
                    TemporaryPassword='LondonBristol#3019',
                    ForceAliasCreation=False,
                    MessageAction='SUPPRESS',
                    DesiredDeliveryMediums=[
                        'EMAIL'
                    ]
                )
         
        print("Native user creation response: ", signup_response)

    response = findExistingUsersByEmail(event, email)
    
    for user in response['Users']:
        provider = None
        provider_value = None
        # Check which provider it is using
        if event['userName'].startswith('Facebook_'):
            provider = 'Facebook'
            provider_value = event['userName'].split('_')[1]
        elif event['userName'].startswith('google_'):
            provider = 'Google'
            provider_value = event['userName'].split('_')[1]

        print('Linking accounts from Email {} with provider {}: '.format(
            email,
            provider_value
        ))

        # If the signup is coming from a social provider, link the accounts
        # with admin_link_provider_for_user function
        if provider and provider_value:
            print('> Linking user: ', user)
            print('> Provider Id: ', provider_value)
            response = client.admin_link_provider_for_user(
                UserPoolId=event['userPoolId'],
                DestinationUser={
                    'ProviderName': 'Cognito',
                    'ProviderAttributeValue': user['Username']
                },
                SourceUser={
                    'ProviderName': provider,
                    'ProviderAttributeName': 'Cognito_Subject',
                    'ProviderAttributeValue': provider_value
                }
            )
    # Return the event to continue the workflow
    return event
    
def findExistingUsersByEmail(event, email):
    return client.list_users(
        UserPoolId=event['userPoolId'],
        AttributesToGet=[
            'email',
        ],
        Filter='email = "{}"'.format(email)
    )
ramit
  • 21
  • 1