Is your feature request related to a problem? Please describe.
Automatic Social Account Merging does not occur by default, https://github.com/aws-amplify/amplify-cli/issues/4208 is the original report, but the author closed it after finding their own solution.
Describe the solution you'd like
Automatically merge social accounts based on one of two criteria:
Describe alternatives you've considered
I could build this myself with a bunch of lambdas and other stuff.
Additional context
This sort of functionality is the default in the majority of authentication services I have used. It was genuinely shocking to discover this was not the case while I was searching through the repo's GithHub issues for a different problem and came across #4208. While I'm sure this is primarily a case of terrible default behaviour in the underlying Cognito service, there is a workaround, and from what I can tell, Amplify is trying to be a "we do the boilerplate stuff for you" type of tool, so handling this kind of workaround/boilerplate feels like something Amplify should be doing.
Thanks for the feature request @techdragon we will investigate merging social accounts by default
Any update on this, as we are have deployed our app and currently, this is an issue for us and now we are stuck at login with apple issue. we do have a website so this feature is mandatory because the user logged in with apple cannot use that in web or android, so the user should be able to login using the given email without any issue.
This seems like such a basic problem that I was also kind of shocked to discover it's not supported by default. There are a bunch of other issues and comments on blog posts asking about this as well.
Apparently the workaround is to use AdminLinkProviderForUser in a pre signup trigger to link to an existing user, but it definitely feels like something Amplify should be handling. As it stands, the same person can sign up with email or an identity provider, forget how they signed up, then sign in with a different identity provider and a new account would be created for them, which is definitely not ideal.
How to verify an account with many social networks?
Then start a session with the different social networks?
Here is an example with the Badoo Social Network.
Any update on this issue?
The way I'm currently handling this is:
In the PreSignup hook, if event.triggerSource
is PreSignUp_ExternalProvider
, I look for an existing user with email event.request.userAttributes.email
using listUsers. Then:
LINKED_EXTERNAL_USER: ${providerName}
. In the frontend I catch this error and reinitiate the OAuth flow, which will log the user in. Normally the end user won't even realize, unless they're logging in with Google and they're signed in to multiple Google accounts on that browser. In that case they'll see the account selector twice, but this will only happen the first time they try to sign in with Google. I haven't been able to work around this.MessageAction: 'SUPRESS'
. After the user is created you need to call adminSetUserPassword with Permanent: true
to change the user state from FORCE_CHANGE_PASSWORD
to CONFIRMED
. Otherwise they won't be able to reset their password with the normal reset password flow (pretty stupid, I know).@ianmartorell Thank you so much for sharing your implementation. It is extremely helpful. I test it and work well. For others who are interested, below is a sample code of mine used for pre sign up lambda based on @ianmartorell's implementation
const aws = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
exports.handler = async (event, context, callback) => {
// ... skip other codes
// If trigger source is external provider
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
const cognitoProvider = new aws.CognitoIdentityServiceProvider({
apiVersion: '2016-04-18',
});
try {
// Get user based on email
const listUserParams = {
UserPoolId: event.userPoolId,
AttributesToGet: null, //null returns all attributes
Filter: `email = \"${event.request.userAttributes.email}\"`,
Limit: 1,
};
const listUsersRes = await cognitoProvider
.listUsers(listUserParams)
.promise();
let destinationAttributeValue;
// If user not found, create user
if (listUsersRes.Users.length === 0) {
console.log('User not found');
const {
email = '',
given_name = '',
family_name = '',
phone_number = '',
} = event.request.userAttributes;
const newPassword = uuidv4(); // or use your own implementation
const newUserParams = {
UserPoolId: event.userPoolId,
Username: email || phone_number,
MessageAction: 'SUPPRESS',
TemporaryPassword: newPassword,
UserAttributes: [
{
Name: 'email',
Value: email,
},
{
Name: 'email_verified',
Value: String(!!email), //auto verify email if provided
},
{
Name: 'given_name',
Value: given_name,
},
{
Name: 'family_name',
Value: family_name,
},
{
Name: 'phone_number',
Value: phone_number,
},
{
Name: 'phone_number_verified',
Value: String(!!phone_number),
},
],
};
const newUser = await cognitoProvider
.adminCreateUser(newUserParams)
.promise();
// Confirm new user
const setPasswordParams = {
Password: newPassword,
UserPoolId: event.userPoolId,
Username: newUser.User.Username,
Permanent: true,
};
await cognitoProvider.adminSetUserPassword(setPasswordParams).promise();
destinationAttributeValue = newUser.User.Username;
}
// If user found, simply set username
else {
console.log('User found');
destinationAttributeValue = listUsersRes.Users[0].Username;
}
// Link User
console.log('Link user');
let [sourceProviderName, sourceAttributeValue] = event.userName.split(
'_'
);
sourceProviderName =
sourceProviderName[0].toUpperCase() + sourceProviderName.slice(1);
const adminLinkParams = {
DestinationUser: {
ProviderAttributeValue: destinationAttributeValue,
ProviderName: 'Cognito',
},
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: sourceAttributeValue,
ProviderName: sourceProviderName,
},
UserPoolId: event.userPoolId,
};
await cognitoProvider.adminLinkProviderForUser(adminLinkParams).promise();
// Finish linking, throw error to frontent
callback(new Error(`LINKED_EXTERNAL_USER_${sourceProviderName}`), event);
} catch (error) {
callback(error, event);
}
}
callback(null, event);
};
Also make sure to add permission to your pre sign up lambda for ListUsers
and other Admins call
"lambdaexecutionpolicy": {
...
"Properties": {
...
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
...
},
{
"Effect": "Allow",
"Action": [
"cognito-idp:ListUsers",
"cognito-idp:AdminLinkProviderForUser",
"cognito-idp:AdminCreateUser",
"cognito-idp:AdminSetUserPassword"
],
"Resource": {
"Fn::Sub": [
"arn:aws:cognito-idp:${region}:${account}:*",
{
"region": {
"Ref": "AWS::Region"
},
"account": {
"Ref": "AWS::AccountId"
}
}
]
}
}
@ianmartorell One of the problem I run into is that calling Auth.federatedSignIn
overwrites Cognito native user attributes. I wonder if you encounter similar issue. Basically, when I created the native user for a user logged in with Facebook, I set email_verified
to true
, but when Auth.federatedSignIn
is called again, email_verified
is set to false
because it reloads the attribues from Facebook to Cognito and Facebook doesn't have the email_verified
attribute apparently. I just created an issue at Amplify.js: https://github.com/aws-amplify/amplify-js/issues/7300
@xitanggg Glad to hear it works for you too! Oh yes, I have that issue with email_verified
as well, I forgot to mention it in my comment. What I do is I automatically confirm and set email_verified
to true for the user in the PostAuthentication
trigger, if they signed in using an external provider. I can check the code I used if that would be helpful.
@ianmartorell Thanks for your prompt response. That is actually what I am thinking as well using AdminUpdateUserAttributes
. I have tried it and it works but I am hesitated to include it in the code because it is so stupid and inefficient since it gets called for every sign in event.
Yeah it's pretty stupid... Unfortunately I don't think there's any other way. As email_verified
comes as false
from external providers for some reason, it gets overwritten on every login. It's quite bothersome. Honestly we have to go through so many hoops and hacks to make social login work with Amplify, it's crazy.
I was able to map google's email_verified
to cognito and it comes with true
. So I only need to do it for facebook. The problem in this case is more in Cognito than Amplify. I am not sure why Cognito is set up to overwrite the attributes https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html. Sad, but I will just move on
Amazon Cognito must be able to update your mapped user pool attributes when users sign in to your application. When a user signs in through an identity provider, Amazon Cognito updates the mapped attributes with the latest information from the identity provider. Amazon Cognito updates each mapped attribute, even if its current value already matches the latest information. If Amazon Cognito can't update the attribute, it throws an error. To ensure that Amazon Cognito can update the attributes, check the following requirements:
Most helpful comment
This seems like such a basic problem that I was also kind of shocked to discover it's not supported by default. There are a bunch of other issues and comments on blog posts asking about this as well.
Apparently the workaround is to use AdminLinkProviderForUser in a pre signup trigger to link to an existing user, but it definitely feels like something Amplify should be handling. As it stands, the same person can sign up with email or an identity provider, forget how they signed up, then sign in with a different identity provider and a new account would be created for them, which is definitely not ideal.