CodeWithYou

How to link a Cognito account with a Google account(source code full stack)

Published on
Authors
How to link a Cognito account with a Google account(Full Stack)
Photo by Flo Karr

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.

Advertisement

All the code is available on GitHub. You can clone the repo and follow the steps below to run the project.

  1. Run yarn install in the cdk and webapp folders.
  2. Rename env.sh.template to env.sh. Update the env.sh file with your own values. 2. GOOGLE_CLIENT_ID - The google client ID. 3. GOOGLE_CLIENT_SECRET - The google client secret.
  3. Run deploy.sh to deploy the backend.
  4. Update the webapp/src/aws-exports.js file with the Cognito user pool information.
  5. Run yarn dev in the webapp folder to start the frontend application.

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.

Advertisement