Custom Authentication with Cognito and React Native
Custom authentication (CUSTOM_AUTH
) with Amazon Cognito enables you to build tailored login flows. In this example, we implement an authentication system based solely on a phone number and an OTP (One-Time Password) sent by SMS. It’s also possible to deliver the code via email. This passwordless approach is especially well-suited for modern mobile-first experiences.
Architecture Overview
We rely on four Lambda functions connected to a Cognito User Pool:
PreSignUp
DefineAuthChallenge
CreateAuthChallenge
VerifyAuthChallengeResponse
These functions form the backend logic of the custom challenge. We connect the flow to a React Native app using the low-level aws-amplify/auth
module without using Amplify CLI.
Cognito User Pool Setup
- Create a Cognito User Pool.
- Enable
CUSTOM_AUTH
as the authentication flow. - Set
phone_number
as a required user attribute. - Attach Lambda triggers as shown below.
- Ensure the account is created and confirmed before
signIn
is called. Otherwise, the challenge Lambdas won’t be invoked.
Lambda: PreSignUp
This function auto-confirms the user and marks the phone number as verified.
⚠️ If the user is not created and confirmed before attempting a
signIn
, theCUSTOM_AUTH
flow will not execute and the challenge Lambdas will not be triggered.
export const handler = async (event: any) => {
event.response.autoConfirmUser = true;
event.response.autoVerifyPhone = true;
return event;
};
✅ Allows signup without a password and auto-verifies phone numbers.
Lambda: DefineAuthChallenge
Controls the state machine of the login flow. This Lambda:
- Accepts success if the last challenge passed.
- Fails after 3 attempts.
- Otherwise requests a new
CUSTOM_CHALLENGE
.
export const handler = async (event: any) => {
if (event.request.session && event.request.session.find((s: any) => s.challengeResult)) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else if (event.request.session.length >= 3) {
event.response.issueTokens = false;
event.response.failAuthentication = true;
} else {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
return event;
};
✅ Drives the challenge logic and limits attempts.
Lambda: CreateAuthChallenge
Generates a 6-digit OTP and sends it via SMS using SNS.
import AWS from 'aws-sdk';
const sns = new AWS.SNS();
export const handler = async (event: any) => {
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
const code = Math.floor(100000 + Math.random() * 900000).toString();
await sns.publish({
Message: `Your login code is: ${code}`,
PhoneNumber: event.request.userAttributes.phone_number,
}).promise();
event.response.publicChallengeParameters = {};
event.response.privateChallengeParameters = { answer: code };
event.response.challengeMetadata = 'OTP Challenge';
}
return event;
};
✅ Sends the code to the user’s phone.
Lambda: VerifyAuthChallengeResponse
Compares the user-submitted OTP with the one generated by CreateAuthChallenge
.
export const handler = async (event: any) => {
const expectedAnswer = event.request.privateChallengeParameters.answer;
const userAnswer = event.request.challengeAnswer;
event.response.answerCorrect = expectedAnswer === userAnswer;
return event;
};
✅ Verifies the validity of the submitted code.
React Native Integration
Install dependencies:
npm install aws-amplify @aws-amplify/auth
Registration Flow
import { signIn, confirmSignIn, signUp } from 'aws-amplify/auth';
const generatePassword = () => Math.random().toString(36).slice(-12) + 'A1!';
export const registration = async (phoneNumber) => {
try {
const result = await signIn({
username: phoneNumber,
options: { authFlowType: 'CUSTOM_WITHOUT_SRP' },
});
console.log("User signed in", result);
return result;
} catch (err) {
if (err.name === 'UserNotFoundException') {
await signup(phoneNumber);
return;
}
throw err;
}
};
const signup = async (phoneNumber) => {
await signUp({
username: phoneNumber,
password: generatePassword(),
options: { userAttributes: { phone_number: phoneNumber } },
});
console.log("User account created");
await registration(phoneNumber);
};
OTP Verification
export const verifyOTP = async (otpCode) => {
const result = await confirmSignIn({ challengeResponse: otpCode });
return result;
};
CDK Deployment Example (without Amplify)
import { Stack, StackProps, CfnOutput, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as path from 'path';
export class AuthStack extends Stack {
public readonly userPoolId: string;
public readonly userPoolClientId: string;
public readonly identityPoolId: string;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
// Define Auth Challenge Lambda
const defineAuthChallengeFunction = new lambda.Function(this, 'DefineAuthChallengeFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-functions/define-auth-challenge')),
timeout: Duration.seconds(30),
});
// Create Auth Challenge Lambda
const createAuthChallengeFunction = new lambda.Function(this, 'CreateAuthChallengeFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-functions/create-auth-challenge')),
timeout: Duration.seconds(30),
});
// Verify Auth Challenge Lambda
const verifyAuthChallengeFunction = new lambda.Function(this, 'VerifyAuthChallengeFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-functions/verify-auth-challenge')),
timeout: Duration.seconds(30),
});
// PreSignUp Lambda
const preSignUpFunction = new lambda.Function(this, 'PreSignUpFunction', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-functions/pre-signup')),
timeout: Duration.seconds(10),
});
// Permissions for SNS (sending SMS) and Cognito (user management)
createAuthChallengeFunction.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'sns:Publish',
],
resources: ['*'],
})
);
createAuthChallengeFunction.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'cognito-idp:AdminGetUser',
'cognito-idp:AdminConfirmSignUp',
],
resources: ['*'],
})
);
// Create the User Pool with Custom Auth Flow
const userPool = new cognito.UserPool(this, 'MyUserPool', {
selfSignUpEnabled: true,
autoVerify: {
email: false,
phone: true,
},
signInAliases: {
phone: true,
email: false,
},
standardAttributes: {
phoneNumber: {
required: true,
mutable: true,
},
},
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
},
// Custom Auth Flow Configuration
lambdaTriggers: {
preSignUp: preSignUpFunction,
defineAuthChallenge: defineAuthChallengeFunction,
createAuthChallenge: createAuthChallengeFunction,
verifyAuthChallengeResponse: verifyAuthChallengeFunction,
},
});
// Donner à Cognito la permission d'invoquer les Lambdas
createAuthChallengeFunction.addPermission('CognitoInvoke', {
principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: userPool.userPoolArn,
action: 'lambda:InvokeFunction',
});
defineAuthChallengeFunction.addPermission('CognitoInvoke', {
principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: userPool.userPoolArn,
action: 'lambda:InvokeFunction',
});
verifyAuthChallengeFunction.addPermission('CognitoInvoke', {
principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: userPool.userPoolArn,
action: 'lambda:InvokeFunction',
});
preSignUpFunction.addPermission('CognitoInvoke', {
principal: new iam.ServicePrincipal('cognito-idp.amazonaws.com'),
sourceArn: userPool.userPoolArn,
action: 'lambda:InvokeFunction',
});
const userPoolClient = new cognito.UserPoolClient(this, 'MyUserPoolClient', {
userPool,
authFlows: {
adminUserPassword: false, // Deactivated with Custom Auth Flow
custom: true, // Activated with Custom Auth Flow
userPassword: false, // Deactivated with Custom Auth Flow
userSrp: false, // Disabled for compatibility with SRP clients
},
oAuth: {
flows: {
authorizationCodeGrant: true,
implicitCodeGrant: true,
},
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PROFILE,
cognito.OAuthScope.PHONE,
],
callbackUrls: ['myapp://'],
},
// Add this configuration to enforce the Custom Auth Flow
generateSecret: false,
preventUserExistenceErrors: false,
});
// Creation of the Identity Pool for mobile authentication
const identityPool = new cognito.CfnIdentityPool(this, 'MyIdentityPool', {
identityPoolName: 'MyIdentityPool',
allowUnauthenticatedIdentities: false,
allowClassicFlow: false,
cognitoIdentityProviders: [
{
clientId: userPoolClient.userPoolClientId,
providerName: userPool.userPoolProviderName,
serverSideTokenCheck: false,
},
],
});
// IAM Role for authenticated users
const authenticatedRole = new iam.Role(this, 'CognitoAuthenticatedRole', {
assumedBy: new iam.FederatedPrincipal(
'cognito-identity.amazonaws.com',
{
StringEquals: {
'cognito-identity.amazonaws.com:aud': identityPool.ref,
},
'ForAnyValue:StringLike': {
'cognito-identity.amazonaws.com:amr': 'authenticated',
},
},
'sts:AssumeRoleWithWebIdentity'
),
});
// IAM Role for unauthenticated users (optional)
const unauthenticatedRole = new iam.Role(this, 'CognitoUnauthenticatedRole', {
assumedBy: new iam.FederatedPrincipal(
'cognito-identity.amazonaws.com',
{
StringEquals: {
'cognito-identity.amazonaws.com:aud': identityPool.ref,
},
'ForAnyValue:StringLike': {
'cognito-identity.amazonaws.com:amr': 'unauthenticated',
},
},
'sts:AssumeRoleWithWebIdentity'
),
});
// Attaching roles to the Identity Pool
new cognito.CfnIdentityPoolRoleAttachment(this, 'IdentityPoolRoleAttachment', {
identityPoolId: identityPool.ref,
roles: {
authenticated: authenticatedRole.roleArn,
unauthenticated: unauthenticatedRole.roleArn,
},
});
// Policy for authenticated users (example: S3 access)
authenticatedRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:GetObject',
's3:PutObject',
's3:DeleteObject',
],
resources: ['arn:aws:s3:::my-user-data/*'],
})
);
// Policy for unauthenticated users (limited access)
unauthenticatedRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
's3:GetObject',
],
resources: ['arn:aws:s3:::my-public-data/*'],
})
);
// Store IDs for reference
this.userPoolId = userPool.userPoolId;
this.userPoolClientId = userPoolClient.userPoolClientId;
this.identityPoolId = identityPool.ref;
// Outputs
new CfnOutput(this, 'UserPoolId', {
value: userPool.userPoolId,
description: 'Cognito User Pool ID',
});
new CfnOutput(this, 'UserPoolClientId', {
value: userPoolClient.userPoolClientId,
description: 'Cognito User Pool Client ID',
});
new CfnOutput(this, 'IdentityPoolId', {
value: identityPool.ref,
description: 'Identity Pool ID for mobile authentication',
});
new CfnOutput(this, 'AuthenticatedRoleArn', {
value: authenticatedRole.roleArn,
description: 'ARN of the role for authenticated users',
});
new CfnOutput(this, 'UnauthenticatedRoleArn', {
value: unauthenticatedRole.roleArn,
description: 'ARN of the role for unauthenticated users',
});
new CfnOutput(this, 'DefineAuthChallengeLambdaArn', {
value: defineAuthChallengeFunction.functionArn,
description: 'ARN of the Define Auth Challenge Lambda',
});
new CfnOutput(this, 'CreateAuthChallengeLambdaArn', {
value: createAuthChallengeFunction.functionArn,
description: 'ARN of the Create Auth Challenge Lambda',
});
new CfnOutput(this, 'VerifyAuthChallengeLambdaArn', {
value: verifyAuthChallengeFunction.functionArn,
description: 'ARN of the Verify Auth Challenge Lambda',
});
}
}
✅ Deploys Cognito and all Lambda triggers using CDK only.
This setup enables a frictionless, secure, and passwordless login experience for your users. With full control via Lambda, you can evolve your authentication logic freely and securely.
Have fun !