Cognito User Pools is an AWS service that provides applications with user identity and auth. This series of articles cover a full stack solution that enables users to sign in with their Email + Password, Google Sign In, or SSO (SAML), and link all methods to the same user within the app:
This article focuses on the logic behind the Cognito Triggers (Lambda Functions) that fire when users register or sign in. This solution allows users to sign in with multiple methods such as Email + Password, Google, and SAML, and have them all link to the same account within the app.
Cognito Triggers
The core of this solution relies on two Cognito Triggers. The high-level flow looks like this:
The Pre sign-up Trigger fires when a user signs up using Google, SAML, or Email + Password. It’s also triggered when an existing user signs in using a new external identity for the first time (e.g. if a user signs up using SAML and then later signs in using Google SSO with the same email).
If they’re signing up with Email + Password, continue.
If they’re signing up via Google or SAML and they haven’t already signed up using a different method with the same email: a native Cognito User is created for that email.
The External Identity is linked to the native Cognito User for that email.
If this is their first time signing in, a User record is created in DynamoDB.
If the user has External Identities linked to their account, the account is marked as verified.
We’ll first look at the code for each trigger and then discuss the details.
Pre Sign-up Lambda Handler
This handler starts by checking the event.triggerSource to determine if the signup is from an External Identity Provider (e.g. Google) or if it’s with Email + Password. If the signup is with Email + Password and the AUTO_VERIFY_USERS environment variable is set, the account is automatically verified and confirmed. The user doesn’t receive a verification email and is instantly signed in, reducing friction during the critical user registration flow.
If the signup is from an External IDP we first get the provider name from the event.userName property. This property’s value looks like 'google_108986458847054040795', so we split on '_' to get the providerName and providerUserId (the user’s ID within the External IDP). We then lookup providerName in the providerNamesLowerCaseLookup map to get the correct capitalization required by Cognito’s adminLinkProviderForUser API. If no matching provider is found within the map, we throw an error.
Next we query Cognito for an existing account under that Email, and if one exists we simply link the External IDP account to it. Otherwise, we create a native Cognito User with a random password and then link the External IDP account.
Pre Token Generation Lambda Handler
This handler’s primary purposes are to create a user record in DynamoDB if one doesn’t exist (syncUserToDynamo), and to ensure the Cognito user remains verified (setUserEmailVerifiedTrue). Both operations are executed in parallel to improve performance thanks to our await Promise.all.
syncUserToDynamo queries DynamoDB for a User item with the Cognito User’s ID (event.request.userAttributes.sub), and if one doesn’t exist it’s inserted into DynamoDB. If a user already exists but doesn’t have a profile picture set and one is available through the sign in method they’re using, it updates DynamoDB with the profile picture (this happens when they first signed in with Email or SAML which doesn’t expose a profile picture, and then signed in with Google). You could also choose to sync other data here, e.g. if you want to update the user’s name or other data to match what’s in the External IDP.
If the Cognito User has External Identities linked to it, setUserEmailVerifiedTrue is called to force the email_verified attribute back to true. There is an unfortunate Cognito “feature” that sets all User attributes to their default value when signing in with an External IDP that doesn’t return that value. For email_verified this is false. We must keep email_verified=true, otherwise the user won’t be able to sign in with their Email + Password or use the “Forgot Password” flow.
getFullName
A small helper function for getting the user’s full name from the External IDP when it doesn’t support mapping one.
End
Ideally, multi-account linking between External Identity Providers and native Cognito Users would be built into Cognito, but thanks to the power of Lambda Triggers it’s something we’re able to implement ourselves.
If you want to build a full stack AWS application that includes functionality like this and much more out of the box, you should try Code Genie 🧞♂️