Bite-Sized Serverless

Cognito Icon

Anonymous User Identities with Cognito Lambda Triggers

Cognito - Intermediate (200)
Cognito User Pools are fully managed user directories for your web and mobile apps. In Cognito's default configuration it requires users to confirm their identity through a valid email address or phone number. In this Bite we will use Cognito Lambda Triggers to avoid personally identifiable information (PII) altogether, allowing for completely anonymous user sign-ups.

Introduction to Cognito User Pools

Most websites and apps allow users to either sign up with a username-password combination or to sign in through a third party like Apple, Google, or Facebook. A website might allow the logged-in user to access specific content or features, purchase items, or communicate with others. These users are called identities. Cognito User Pools are an Identity Provider (IdP): a service that provides sign-up and sign-in functionality, safely stores passwords, can organize users in groups, and enables password reset and MFA features.
When users sign in to a Cognito User Pool they receive Access, ID, and Refresh tokens in the form of JSON Web Tokens (JWT). These tokens can be stored in a browser's local storage for long-lived sessions. The Access token is used to identify the user in API requests, for example when fetching paid articles from a backend. Bite-Sized Serverless uses Cognito User Pools to allow signed-in users to access their purchased Bites.

User sign-up

Most websites allow users to create their accounts. They require either an email address or username, and a password. Naturally, Cognito User Pools support these sign-up flows too. The following CDK code configures a Cognito User Pool with a mandatory email address. All examples in this Bite are available in a CDK project available for download at the bottom of this page. The project also contains a simple user interface built in Gatsby, allowing for interactions with the User Pools.
1# Create a userpool which requires an email address and verification 2user_pool_default = cognito.UserPool( 3 scope=self, 4 id="UserPool", 5 self_sign_up_enabled=True, 6 account_recovery=cognito.AccountRecovery.EMAIL_ONLY, 7 sign_in_aliases=cognito.SignInAliases( 8 email=True, 9 phone=False, 10 username=False, 11 ), 12 removal_policy=cdk.RemovalPolicy.DESTROY, 13)
When a user signs up to a Cognito User Pool configured like the one above, the User Pool will send out a verification email directly after the sign-up. This email might look like this.
1The verification code for your account is 777541.
Meanwhile, the website will display a screen asking for this verification code. The user provides their code to the website, verifying they have access to the provided email address. If the user tries to log in before they verified their account, Cognito returns an error.
When the verification step has been completed, Cognito updates the user's status to confirmed. The user is now allowed to sign in to the website. The following diagram displays an overview of the Cognito User Pools sign-up and sign-in process.

Anonymous users

The default Cognito auth flow requires users to provide some sort of verification of their identity - either an email address or phone number. But what if you're building a site where users should be anonymous, like a whistleblower site? For these applications, you want users to create an arbitrary username, and you do not want to store any information which can lead back to your users. This can be achieved with Cognito using Cognito Lambda Triggers.
Cognito Lambda Triggers are common Lambda Functions like any other. Cognito User Pools can be configured to call these Lambda Functions when certain events occur, like a user signing up. There are 12 Cognito Lambda Triggers. An overview can be found in the table below.
User Pool FlowOperationDescriptionDocumentation
Custom Authentication FlowDefine Auth ChallengeDetermines the next challenge in a custom auth flowlink
Create Auth ChallengeCreates a challenge in a custom auth flowlink
Verify Auth Challenge ResponseDetermines if a response is correct in a custom auth flowlink
Authentication EventsPre Authentication Lambda TriggerCustom validation to accept or deny the sign-in requestlink
Post Authentication Lambda TriggerEvent logging for custom analyticslink
Pre Token Generation Lambda TriggerAugment or suppress token claimslink
Sign-UpPre Sign-Up Lambda TriggerCustom validation to accept or deny the sign-up requestlink
Post Confirmation Lambda TriggerCustom welcome messages or event logging for custom analyticslink
Migrate User Lambda TriggerMigrate a user from an existing user directory to user poolslink
MessagesCustom Message Lambda TriggerAdvanced customization and localization of messageslink
Token CreationPre Token Generation Lambda TriggerAdd or remove attributes in Id tokenslink
Email and SMS third-party providersCustom Sender Lambda TriggersUse a third-party provider to send SMS and email messageslink
Each of these Lambda Functions is optional: they are only included in the sign-in or sign-up flow when configured. For our anonymous Cognito User Pool we will focus on the Pre Sign-Up Lambda Trigger. To enable the Lambda Trigger for our User Pool, we update the CDK code as shown below.
1# Create a userpool which does not require an email address or verification 2user_pool_no_verify = cognito.UserPool( 3 scope=self, 4 id="UserPoolNoVerify", 5 self_sign_up_enabled=True, 6 account_recovery=cognito.AccountRecovery.NONE, 7 sign_in_aliases=cognito.SignInAliases( 8 email=False, 9 phone=False, 10 username=True, 11 ), 12 removal_policy=cdk.RemovalPolicy.DESTROY, 13) 14 15# Create a sign-up Lambda Function for the Pre Sign-Up Trigger 16pre_sign_up_lambda_function = LambdaFunction( 17 scope=self, 18 construct_id="PreSignUpLambda", 19 code=lambda_.Code.from_asset("lambda_functions/pre_sign_up"), 20) 21 22# Allow the Lambda Function to be executed by the Cognito User Pool 23pre_sign_up_lambda_function.function.add_permission( 24 scope=self, 25 id="CognitoLambdaPermission", 26 action="lambda:InvokeFunction", 27 principal=iam.ServicePrincipal(service="cognito-idp.amazonaws.com"), 28 source_arn=user_pool_no_verify.user_pool_arn, 29) 30 31# Add the Lambda Function as a Pre Sign-Up Trigger 32cfn_user_pool: cognito.CfnUserPool = user_pool_no_verify.node.default_child 33cfn_user_pool.lambda_config = cognito.CfnUserPool.LambdaConfigProperty( 34 pre_sign_up=pre_sign_up_lambda_function.function.function_arn 35)
The Lambda Function receives the following payload when a user signs up.
1{ 2 "version": "1", 3 "region": "eu-west-1", 4 "userPoolId": "eu-west-1_8XVE45sSD", 5 "userName": "luc", 6 "callerContext": { 7 "awsSdkVersion": "aws-sdk-js-3.45.0", 8 "clientId": "63e6a5emisd8ecpdf3c0io401n" 9 }, 10 "triggerSource": "PreSignUp_SignUp", 11 "request": { 12 "userAttributes": {}, 13 "validationData": null 14 }, 15 "response": { 16 "autoConfirmUser": false, 17 "autoVerifyEmail": false, 18 "autoVerifyPhone": false 19 } 20}
To enable anonymous users, the Lambda Function sets the response.autoConfirmUser field to True. With autoConfirmUser enabled, any user signing up is immediately confirmed, without validation emails or text messages.
1import json 2 3def event_handler(event, _context): 4 print(json.dumps(event)) 5 6 event["response"]["autoConfirmUser"] = True 7 return event
With these components in place, a user is confirmed immediately after sign-up. When a new user tries to sign in the attempt will succeed without further verification.
The sign-up flow now looks like the diagram below.

Other Cognito Lambda Triggers

In the example above we looked at the details of the Pre Sign-Up Lambda Trigger. We have seen that the Lambda Function receives an object, modifies one or more fields in the object, and returns the results to modify internal Cognito processes. The other 11 Lambda Triggers also receive contextual information in an object payload, but their response does not always take the same form.
For example, the Pre Authentication Lambda Trigger receives the following payload.
1{ 2 "version": "1", 3 "region": "eu-west-1", 4 "userPoolId": "eu-west-1_Yomn919TM", 5 "userName": "user@example.com", 6 "callerContext": { 7 "awsSdkVersion": "aws-sdk-js-3.45.0", 8 "clientId": "421v3tjtf7fgcb2g33q2ef7lr1" 9 }, 10 "triggerSource": "PreAuthentication_Authentication", 11 "request": { 12 "userAttributes": { 13 "sub": "c6c30aab-9b44-4aed-b6f8-5857519243c2", 14 "cognito:user_status": "CONFIRMED" 15 }, 16 "validationData": null 17 }, 18 "response": {} 19}
When the Lambda Function approves the authentication attempt it returns the original event. When the authentication event is blocked, the Lambda Function raises an error. The following Python code raises an exception when a user has an example.com email address.
1import json 2 3 4def event_handler(event, _context): 5 print(json.dumps(event)) 6 7 username: str = event["userName"] 8 9 # If username contains an '@', partition() will return a 10 # 3-tuple containing the part before the '@', the '@' itself 11 # and the part after the '@'. If the '@' is not found, a 3-tuple 12 # containing the original string and two empty strings is returned. 13 email_components = username.partition("@") 14 15 if email_components[2] == "example.com": 16 raise Exception("Users from example.com are no longer allowed to log in") 17 18 return event
With this Pre Authentication Lambda Trigger in place, Cognito returns an error when users with blocked email addresses attempt to log in.
This Lambda Function could be extended to look up users in an external source. This source could, for example, be a DynamoDB Table used to maintain a ban or block list.
An overview of various sign-in and sign-up triggers can be found in the diagram below.

Conclusion

Cognito Lambda Triggers are a powerful way to change how Cognito User Pools handle user interactions. In this Bite we looked at auto-verification and blocking user sign-ins, but there are many other Lambda Triggers to explore. Because the Cognito Lambda Triggers run like any other Lambda Function, they can interact with external systems, allowing for powerful integrations.

CDK Project

The services and code described in this Bite are available as a Python AWS Cloud Development Kit (CDK) Project. Within the project, execute a cdk synth to generate CloudFormation templates. Then deploy these templates to your AWS account with a cdk deploy. For your convenience, ready-to-use CloudFormation templates are also available in the cdk.out folder. For further instructions how to use the CDK, see Getting started with the AWS CDK.

Click the Download button below for a Zip file containing the project.