Borislav Hadzhiev
Reading time·4 min
Photo from Unsplash
We need to verify a Cognito user's email because otherwise, they can't use the forget password and other functionality.
In order to verify a Cognito user's email, we have to set their
email_verified
attribute to true
. To set their email_verified
attribute to
true
we can use the admin-update-user-attributes
command.
aws cognito-idp admin-update-user-attributes --user-pool-id YOUR_USER_POOL_ID --username john@gmail.com --user-attributes Name="email_verified",Value="true"
We're using the admin-update-user-attributes command to verify the user's email, however, it can be used to update any of the Cognito user's attributes.
To make sure the email of the user is verified, run the admin-get-user command.
aws cognito-idp admin-get-user --user-pool-id YOUR_USER_POOL_ID --username john@gmail.com --query "UserAttributes"
The admin-get-user
command returns information about the Cognito user, however
we've filtered the output to only show the user's attributes, by setting the
--query
parameter.
Once the user's email_verified
attribute is set to true
, they can use the
forget password functionality and get emails with confirmation codes.
When using Facebook OAuth by default the email_verified
attribute is set to
false
.
By setting the email_verified
attribute to true we can use functionality like
Forgotten Password for Cognito native (email) accounts that are linked to the
Facebook OAuth account.
According to AWS Support the best way to handle Facebook email verification is by using the Post Authentication Lambda trigger - support comment
AWS Cognito invokes the Post Authentication Lambda trigger after a user signs
in. In the function we would have to update the email_verified
attribute using
the AdminUpdateUserAttributes
API.
It's important to note that, as the name Post Authentication suggests, this trigger is run every time the user logs in.
It would be more natural to handle something like this in the Post Confirmation Lambda Trigger, which runs only after a user has successfully been registered. However, that wouldn't work because you would get a race condition
Let's see how we can implement the Post Authentication Lambda trigger.
Let's define the email confirmation lambda function:
import AWS from 'aws-sdk'; import {Callback, Context, PostAuthenticationTriggerEvent} from 'aws-lambda'; import {adminUpdateUserAttributes} from './admin-update-user-attributes'; const adminUpdateUserAttributes = async ({ userPoolId, username, }: { userPoolId: string; username: string; }): Promise<AWS.CognitoIdentityServiceProvider.AdminUpdateUserAttributesResponse> => { const params = { UserPoolId: userPoolId, Username: username, UserAttributes: [{Name: 'email_verified', Value: 'true'}], }; const cognitoIdp = new AWS.CognitoIdentityServiceProvider(); return cognitoIdp.adminUpdateUserAttributes(params).promise(); }; export async function main( event: PostAuthenticationTriggerEvent, _context: Context, callback: Callback, ): Promise<void> { const {userPoolId, userName} = event; console.log('POST CONFIRMATION EVENT', JSON.stringify(event, null, 2)); if (event.request.userAttributes.email) { const identities = event.request?.userAttributes?.identities; const isExternalUser = /providername.*facebook/gi.test(identities) || /providername.*google/gi.test(identities); if (isExternalUser) { try { await adminUpdateUserAttributes({ userPoolId, username: userName, }); return callback(null, event); } catch (error) { console.log( 'POST AUTHENTICATION ERROR: ', JSON.stringify(error, null, 2), ); return callback(error, event); } } } return callback(null, event); }
We basically check if the user who's signing in is External (i.e. Facebook or Google - you can only check for the one you use).
If the user is external, we call adminUpdateUserAttributes
, which sets the
email_verified
property to true.
If the user is not an external one, we just return the callback, without doing anything.
cognito-idp:AdminUpdateUserAttributes
action to call the necessary API.This policy grants the necessary permissions:
{ "Version": "2012-10-17", "Statement": [ { "Action": "cognito-idp:AdminUpdateUserAttributes", "Resource": "YOUR_USER_POOL_ARN", "Effect": "Allow" } ] }
After you've defined the Lambda function you can set it as a Post Authentication Lambda trigger in your User Pool.
This is definitely one of the rough edges around Cognito as it doesn't make much sense to confirm a user's email every time they log in to your application.
However, this is the current way to handle this and at least in my implementation I haven't experienced any issues.
When you use AWS Cognito and Amplify with Google OAuth user emails are not automatically verified.
In other words, the email_verified
attribute is set to false
for Google
registered users.
We need users to verify their email because otherwise, we can't use the forgot password functionality. This is very important in case you support Cognito native email login and link the accounts.
In order to verify the emails of Google OAuth accounts in Cognito, we have to
provide an attribute mapping between Google's email_verified
attribute and
Cognito's email_verified
attribute.
After the email_verified
attribute has been mapped between Google and Cognito,
we can leverage the existing email_verified
property on the user's Google
account.
Here's how the attribute mapping looks in the AWS Console:
The following snippet shows how to set the attribute mapping in AWS CDK:
this.identityProviderGoogle = new cognito.UserPoolIdentityProviderGoogle( this, 'userpool-identity-provider-google', { // ... other config attributeMapping: { email: { attributeName: cognito.ProviderAttribute.GOOGLE_EMAIL.attributeName, }, custom: { email_verified: cognito.ProviderAttribute.other('email_verified'), }, }, }, );
Once we map the email_verified
attribute we can leverage the existing property
on the user's google account.
The most intuitive way would be if the email_verified
property of Google
accounts were set to true
by default, but that's currently not the case.