Search code examples
amazon-web-servicesamazon-cognitoaws-amplifyfederated-identity

AWS Amplify: How to map social providers attributes for an email based authentication?


I have a React web application built with AWS Amplify I have added authentication with Cognito user pools, I am not using usernames, I have selected the login using email/phone only, I don't want usernames, but Cognito creates a random username anyway.

I want that the user to login using their email or using one social provider (Facebook or Google) and it shouldn't matter, they should have access to the same account, based on the email.

Pretty normal practice, I think. However, when I first tried to log in with a social provider I noticed that instead of Cognito gives me the same account, it created a new one, with a different username and with EXTERNAL_PROVIDER as account status on the user pool

So, I thought it might be just some mapping being made incorrectly, I went to the Attributes mapping on the Federation section, and I saw that the Facebook Id and Google Sub were being assigned to the username, I tried to remove it and for my surprise, it was assigned back to the username. Then I thought "I can just create a custom attribute to store that information and it should be fine".

So I did this, created one attribute for Facebook, one for GoogleId... tried again...nope, still getting back to username, but no error message, nothing...

I went for help on the documentation and found this

Currently, only the Facebook id, Google sub, login with Amazon user_id, and Sign in with Apple sub attributes can be mapped to the Amazon Cognito User Pools username attribute.

If this always associate each provider Id with the username, then, there's no way to merge those accounts I thought it could be that I need to setup a federated identity pool, but reading about it it seems that it is used to give IAM roles/permission to the external users, which I don't want to.

Any idea of how can I achieve that?


Solution

  • After digging a bit into it, I found the solution. In summary, you should:

    • Create a trigger on Cognito for the Pre-Signup to a lambda function
    • This lambda function should find the corresponding account and link both users
    • Return the event from the lambda function

    You can create the trigger from Amplify cli, running amplify auth update, doing the "Walkthrough all the auth configurations", in the end, it is gonna ask you if you want to create the trigger, confirm it and select the Pre Signup Trigger

    Then edit the function file created, the default runtime is nodejs, I have changed mine to Python

    This is the code I'm using

    import boto3
    
    client = boto3.client('cognito-idp')
    
    
    def handler(event, context):
        print("Event: ", event)
        email = event['request']['userAttributes']['email']
    
        # Find a user with the same email
        response = client.list_users(
            UserPoolId=event['userPoolId'],
            AttributesToGet=[
                'email',
            ],
            Filter='email = "{}"'.format(email)
        )
    
        print('Users found: ', response['Users'])
    
        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