How to link a Cognito account with a Google account(source code full stack)
- Published on
- Authors
- Name
- Binh Bui
- @bvbinh
Introduction
Recently, I worked on a project that required users to be able to sign in with their Google account and users to be able to link their Google account with their Cognito account. I found that there is not much information on the internet about how to do this. So I decided to write this article to share my experience. I will show you how to link a Cognito account with a Google account in the front end and back end.
Links an existing user account in a user pool (DestinationUser) to an identity from an external IdP (SourceUser) based on a specified attribute name and value from the external IdP. 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. You can then use the federated user identity to sign in as the existing user account.
For example, if there is an existing user with a username and password, this API links that user to a federated user identity. When the user signs in with a federated user identity, they sign in as the existing user account.
By default, AWS Cognito does not support linking social account (google/facebook) to AWS Cognito user. This project is to provide a solution to link social account to AWS Cognito user. The solution is to use pre-signup
trigger to check if the user already exists in the user pool. If the user already exists, then link the social account to the user. If the user does not exist, then create a new user in the user pool.
All the code is available on GitHub. You can clone the repo and follow the steps below to run the project.
- Run
yarn install
in thecdk
andwebapp
folders. - Rename
env.sh.template
toenv.sh
. Update theenv.sh
file with your own values. 2.GOOGLE_CLIENT_ID
- The google client ID. 3.GOOGLE_CLIENT_SECRET
- The google client secret. - Run
deploy.sh
to deploy the backend. - Update the
webapp/src/aws-exports.js
file with the Cognito user pool information. - Run
yarn dev
in thewebapp
folder to start the frontend application.
How to link a Cognito account with a IDP account
The key to link a Cognito account with a IDP account is to use the pre-signup
trigger. In the pre-signup
trigger, we can check if the user already exists in the user pool. If the user already exists, then link the IDP account to the user. If the user does not exist, then create a new user in the user pool.
To order to link a Cognito account with a IDP account, we need to use the adminLinkProviderForUser
API. The adminLinkProviderForUser
API is used to link an existing user account in a user pool (DestinationUser) to an identity from an external IdP (SourceUser) based on a specified attribute name and value from the external IdP. 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. You can then use the federated user identity to sign in as the existing user account.
Below is the code for the pre-signup
trigger. The pre-signup
trigger is used to check if the user already exists in the user pool. If the user already exists, then link the IDP account to the user. If the user does not exist, then create a new user in the user pool.
export const preSignup: PreSignUpTriggerHandler = async (event: PreSignUpTriggerEvent) => {
console.log('preSignup event', event)
const { triggerSource, userPoolId, userName, request } = event
// Note: triggerSource can be either PreSignUp_SignUp or PreSignUp_ExternalProvider depending on how the user signed up
// incase signup is done with email and password then triggerSource is PreSignUp_SignUp
// incase signup is done with Google then triggerSource is PreSignUp_ExternalProvider
if (triggerSource === 'PreSignUp_ExternalProvider') {
// if user signed up with Google then we need to link the Google account to the user pool
const {
userAttributes: { email, given_name, family_name },
} = request
// if the user is found then we link the Google account to the user pool
const user = await findUserByEmail(email, userPoolId)
// userName example: "Facebook_12324325436" or "Google_1237823478"
// we need to extract the provider name and provider value from the userName
let [providerName, providerUserId] = userName.split('_')
// Uppercase the first letter because the event sometimes
// has it as google_1234 or facebook_1234. In the call to `adminLinkProviderForUser`
// the provider name has to be Google or Facebook (first letter capitalized)
providerName = providerName.charAt(0).toUpperCase() + providerName.slice(1)
// if the user is found then we link the Google account to the user pool
if (user) {
await linkSocialAccount({
userPoolId: userPoolId,
cognitoUsername: user.Username,
providerName: providerName,
providerUserId: providerUserId,
})
// return the event to continue the signup process
return event
} else {
// if the user is not found then we need to create the user in the user pool
// 1. create a native cognito account
const newUser = await createUser({
userPoolId: userPoolId,
email,
givenName: given_name,
familyName: family_name,
})
if (!newUser) {
throw new Error('Failed to create user')
}
// 2. change the password, to change status from FORCE_CHANGE_PASSWORD to CONFIRMED
await setUserPassword({
userPoolId: userPoolId,
email,
})
// 3. merge the social and the native accounts
await linkSocialAccount({
userPoolId: userPoolId,
cognitoUsername: newUser.Username,
providerName: providerName,
providerUserId: providerUserId,
})
// set the email_verified to true so that the user doesn't have to verify the email
// set the autoConfirmUser to true so that the user doesn't have to confirm the signup
event.response.autoVerifyEmail = true
event.response.autoConfirmUser = true
}
}
// if the user signed up with email and password then we don't need to do anything
return event
}
const linkProviderForUserCommand = new AdminLinkProviderForUserCommand({
UserPoolId: userPoolId,
DestinationUser: {
ProviderName: 'Cognito', // Cognito is the default provider
ProviderAttributeValue: cognitoUsername, // this is the username of the user
},
SourceUser: {
ProviderName: providerName, // Google or Facebook (first letter capitalized)
ProviderAttributeName: 'Cognito_Subject', // Cognito_Subject is the default attribute name
ProviderAttributeValue: providerUserId, // this is the value of the provider
},
})
await cognitoIdentityProviderClient.send(linkProviderForUserCommand)
By default, every user sign up with social identity providers (Google/Facebook) will have a Cognito_Subject
attribute. The Cognito_Subject
attribute is the unique identifier of the user in the social identity provider. We can use the Cognito_Subject
attribute to link the social account to the Cognito user. The Cognito_Subject
attribute is the same as the sub
claim in the ID token.
Troubleshooting
1. "Already found an entry for username" exception when linking a user
This is known issue and discussing in this. The solution is catch this error in your front end, perhaps show a message like "Accounts have been successfully linked" and then prompt users to re-login with Hosted UI. This is hacky
solution but no other solution at the moment. I hope AWS will fix this issue in the future.
2. Every user sign-in with social identity providers (Google/Facebook) then the email becomes unverified
This is very annoying. After a user signs in with social identity providers (Google/Facebook), the email address becomes unverified. It makes the user have to verify the email address again every time they sign in username and password.
This is default behavior of AWS Cognito. When a user signs up with social identity providers (Google/Facebook), the email address is not verified. And it will update every time the user signs in with social identity providers (Google/Facebook). The solution is to use the post-authentication
trigger to verify the email address.
export const postAuthentication = async (event: PostAuthenticationTriggerEvent) => {
console.log('postAuthentication event', event)
// set email_verified to true so that the user doesn't have to verify the email
if (event.request.userAttributes.email_verified !== 'true') {
await updateUserAttributes(event.userPoolId, event.userName, {
email_verified: 'true',
})
}
return event
}
Conclusion
Merge accounts with Cognito User Pools and Identity Providers still too complicated. Some developers are not happy with this feature. I hope AWS will improve this feature in the future. And I hope this article will help you to link a Cognito account with a IDP account.
You can find the source code of this article in this. If you have any questions, please leave a comment below. I will try to answer your questions. Thank you for reading this article.