Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save vdelacou/1dccafd9516b13c4079df0caa44c5c70 to your computer and use it in GitHub Desktop.
Save vdelacou/1dccafd9516b13c4079df0caa44c5c70 to your computer and use it in GitHub Desktop.
Passwordless Cognito With email or Phone
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"mobiletargeting:SendMessages"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:mobiletargeting:",
{
"Ref": "analyticstutoRegion"
},
":",
{
"Ref": "AWS::AccountId"
},
":apps/",
{
"Ref": "analyticstutoId"
},
"/messages"
]
]
},
{
"Fn::Join": [
"",
[
"arn:aws:ses:",
{
"Ref": "analyticstutoRegion"
},
":",
{
"Ref": "AWS::AccountId"
},
":identity/",
{
"Ref": "analyticstutoId"
}
]
]
}
]
}
]
}
import { CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler, Context } from 'aws-lambda';
import { Pinpoint } from 'aws-sdk';
import { randomDigits } from 'crypto-secure-random-digit';
import i18n from 'i18n';
const logoUrl = `https://www.example.com/icons/icon-48x48.png`;
const emailSupport = `[email protected]`;
const webSiteUrl = `https://www.example.com`;
// Get Pinpoint Project ID from environment variable
const poinpointProjectID = process.env.ANALYTICS_BACKEND_ID;
if (!poinpointProjectID) {
throw new Error(`Function requires environment variable: 'ANALYTICS_BACKEND_ID'`);
}
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, context: Context) => {
i18n.configure({
defaultLocale: 'fr',
directory: __dirname + '/locales',
});
// set the locale
if (event.request.userAttributes.hasOwnProperty('locale')) {
i18n.setLocale(event.request.userAttributes.locale);
}
// only for CUSTOM_CHALLENGE
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
// if we are in the custom challenge, and no session exists
if (event.request.session === undefined || event.request.session.length === 0) {
// if account created with phone number
if ('cognito:phone_number_alias' in event.request.userAttributes) {
const phoneNumber = event.request.userAttributes['cognito:phone_number_alias'];
// we create a 6 digits code
const secretLoginCode = randomDigits(6).join('');
// send SMS with the code
await sendSms(phoneNumber, secretLoginCode);
// This is sent back to the client app, so they know if it's email or phone challenge
event.response.publicChallengeParameters = { phone: phoneNumber };
// Add the secret login code to the private challenge parameters so it can be verified by the "Verify Auth Challenge Response" trigger
event.response.privateChallengeParameters = { secretLoginCode };
// Add the secret login code to the session so it is available in the next
event.response.challengeMetadata = `CODE-${secretLoginCode}`;
}
// if account created with email
if ('cognito:email_alias' in event.request.userAttributes) {
const emailAddress = event.request.userAttributes['cognito:email_alias'];
// we create a 6 digits code
const secretLoginCode = randomDigits(6).join('');
// send email with the code
await sendEmail(emailAddress, secretLoginCode);
// This is sent back to the client app, so they know if it's email or phone challenge
event.response.publicChallengeParameters = { email: emailAddress };
// Add the secret login code to the private challenge parameters so it can be verified by the "Verify Auth Challenge Response" trigger
event.response.privateChallengeParameters = { secretLoginCode };
// Add the secret login code to the session so it is available in the next
event.response.challengeMetadata = `CODE-${secretLoginCode}`;
}
} else {
// we already have session. So we don't generate digits again but re-use the code from the current session.
// This allows the user to make a mistake when keying in the code and to then retry, rather the needing to resend email or sms with new code
const previousChallenge = event.request.session.slice(-1)[0];
if (previousChallenge.challengeMetadata !== undefined) {
// we do regex on the previous Challenge Medadata
const previousSecretLoginCodeRegexMacthArray = previousChallenge.challengeMetadata.match(/CODE-(\d*)/);
if (previousSecretLoginCodeRegexMacthArray !== null && previousSecretLoginCodeRegexMacthArray.length === 2) {
// we get the previous secret code and resend it
const previousSecretLoginCode = previousSecretLoginCodeRegexMacthArray[1];
event.response.privateChallengeParameters = { secretLoginCode: previousSecretLoginCode };
event.response.challengeMetadata = `CODE-${previousSecretLoginCode}`;
// we send back the phone or email to client
if ('cognito:phone_number_alias' in event.request.userAttributes) {
const phoneNumber = event.request.userAttributes['cognito:phone_number_alias'];
event.response.publicChallengeParameters = { phone: phoneNumber };
}
if ('cognito:email_alias' in event.request.userAttributes) {
const emailAddress = event.request.userAttributes['cognito:email_alias'];
event.response.publicChallengeParameters = { email: emailAddress };
}
}
}
}
}
context.done(undefined, event);
};
const sendEmail = async (emailAddress: string, secretLoginCode: string) => {
const pinpoint = new Pinpoint();
const sendMessagesRequest: Pinpoint.SendMessagesRequest = {
ApplicationId: poinpointProjectID,
MessageRequest: {
Addresses: {
[emailAddress]: {
ChannelType: 'EMAIL',
},
},
MessageConfiguration: {
EmailMessage: {
SimpleEmail: {
Subject: {
Charset: 'UTF-8',
Data: `${i18n.__l('email.subject')} ${secretLoginCode}`,
},
HtmlPart: {
Charset: 'UTF-8',
Data: emailTemplate(secretLoginCode),
},
TextPart: {
Charset: 'UTF-8',
Data: emailTemplateText(secretLoginCode),
},
},
},
},
},
};
const messagePromise = pinpoint.sendMessages(sendMessagesRequest);
try {
const response = await messagePromise.promise();
if (response.$response.error) {
console.error(JSON.stringify(response.$response.error));
}
} catch (err) {
console.error(err, err.stack);
}
};
const sendSms = async (phoneNumber: string, secretLoginCode: string) => {
const pinpoint = new Pinpoint();
const sendMessagesRequest: Pinpoint.SendMessagesRequest = {
ApplicationId: poinpointProjectID,
MessageRequest: {
Addresses: {
[phoneNumber]: {
ChannelType: 'SMS',
},
},
MessageConfiguration: {
SMSMessage: {
Body: `${i18n.__l('sms.subject')} ${secretLoginCode}`,
MessageType: 'TRANSACTIONAL',
},
},
},
};
const messagePromise = pinpoint.sendMessages(sendMessagesRequest);
try {
const response = await messagePromise.promise();
if (response.$response.error) {
console.error(JSON.stringify(response.$response.error));
}
} catch (err) {
console.error(err, err.stack);
}
};
const emailTemplate = (otpCode: string) => {
return `
<html>
<body>
<center>
<table>
<tbody>
<tr>
<td style="vertical-align:top; padding:0px">
<table
style="border:0px; border-collapse:collapse; margin:0px auto 16px; background:white ;border-radius:8px">
<tbody>
<tr>
<td style="width:546px; vertical-align:top; padding-top:32px">
<div style="max-width:600px; margin:0px auto">
<img style="width:38px; height:38px; margin:0px 0px 15px; padding-right:30px; padding-left:30px;" src="${logoUrl}" width="38" height="38">
<h1 style="font-size:30px; padding-right:30px; padding-left:30px">
${i18n.__l('email.title')}
</h1>
<p style="font-size:17px;padding-right:30px;padding-left:30px">
${i18n.__l('email.subTitle1')}
</p>
<p style="font-size:17px;padding-right:30px;padding-left:30px">
${i18n.__l('email.subTitle2')}
</p>
<div style="padding-right:30px; padding-left:30px; margin:32px 0px 40px">
<center>
<table style="border-collapse:collapse; border:0px; background-color:rgb(244,244,244); height:70px; table-layout:fixed; border-radius:6px">
<tbody>
<tr>
<td style="text-align:center; vertical-align:middle; font-size:30px; letter-spacing: 0.5rem; padding-left:0.7rem;">
${otpCode}
</td>
</tr>
</tbody>
</table>
</center>
</div>
<p style="font-size:17px; padding-right:30px; padding-left:30px">
${i18n.__l('email.disclaimer')} <a style="color:rgb(5,118,185)" href="mailto:${emailSupport}" target="_blank">${emailSupport}</a>
</p>
</div>
</td>
</tr>
</tbody>
</table>
<center>
<table style="width:100%;">
<tbody>
<tr>
<td style="font-size:15px; color:rgb(113,114,116); text-align:center; width:100%; border-top:1px solid rgb(225,225,228)">
<center>
<table style="margin-top:20px; background-color:white; border:0px; text-align:center ;border-collapse:collapse">
<tbody>
<tr>
<td>
<span style="display:block; color:rgb(67,66,69); font-size:15px">
${i18n.__l('email.footerCreatedBy')}
<a href="${webSiteUrl}" style="text-decoration:none; color:rgb(67,66,69)" target="_blank">
${i18n.__l('email.footerCreatedByCompany')}
</a>
</span>
</td>
</tr>
</tbody>
</table>
</center>
</td>
</tr>
</tbody>
</table>
</center>
</td>
</tr>
</tbody>
</table>
</center>
</body>
</html>
`;
};
const emailTemplateText = (otpCode: string) => {
return `
${i18n.__l('email.title')}\n\n
${i18n.__l('email.subTitle1')}\n
${i18n.__l('email.subTitle2')}\n\n\n
${i18n.__l('otpCode')}\n\n\n
${i18n.__l('email.disclaimer')} ${emailSupport}\n
`;
};
{
"version": "1",
"region": "us-east-1",
"userPoolId": "us-east-XXXX",
"userName": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "XXXXXXXXXXXXXXXXXXXXXXXXX"
},
"triggerSource": "CreateAuthChallenge_Authentication",
"request": {
"userAttributes": {
"sub": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"cognito:user_status": "CONFIRMED",
"phone_number_verified": "false",
"cognito:phone_number_alias": "+861234567890",
"phone_number": "+861234567890"
},
"challengeName": "CUSTOM_CHALLENGE",
"session": []
},
"response": {
"publicChallengeParameters": null,
"privateChallengeParameters": null,
"challengeMetadata": null
}
}
import { CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler, Context } from 'aws-lambda';
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, context: Context) => {
// if we don't have session, it's the first time we try to login so we propose the CUSTOM_CHALLENGE
if (event.request.session === undefined || event.request.session.length === 0) {
event.response.issueTokens = false;
event.response.failAuthentication = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
} else {
// we authorize with the CUSTOM_CHALLENGE
if (event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE') {
// reply that we are in CUSTOM_CHALLENGE context
event.response.challengeName = 'CUSTOM_CHALLENGE';
// can try to answer the custom challenge only 3 times (if user do mistake when copy the secret code)
if (event.request.session.length < 3) {
// if the last answer is correct, we issue the token and go to the next step
if (event.request.session.slice(-1)[0].challengeResult === true) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
// wrong answer, so we let the user answer again
event.response.issueTokens = false;
event.response.failAuthentication = false;
}
} else {
// if try to answer more than 3 times we reject authentication
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
} else {
// if not CUSTOM_CHALLENGE we reject authentication
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
}
context.done(undefined, event);
};
{
"version": "1",
"region": "us-east-1",
"userPoolId": "us-east-XXXX",
"userName": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "XXXXXXXXXXXXXXXXXXXXXXXXX"
},
"triggerSource": "DefineAuthChallenge_Authentication",
"request": {
"userAttributes": {
"sub": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"cognito:user_status": "CONFIRMED",
"phone_number_verified": "false",
"cognito:phone_number_alias": "+861234567890",
"phone_number": "+861234567890"
},
"session": []
},
"response": {
"challengeName": null,
"issueTokens": null,
"failAuthentication": null
}
}
import { Callback, CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler } from 'aws-lambda';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider();
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, _, callback: Callback<CognitoUserPoolTriggerEvent>) => {
if (event.userName) {
// if account created with phone
if ('cognito:phone_number_alias' in event.request.userAttributes) {
const params: CognitoIdentityServiceProvider.AdminUpdateUserAttributesRequest = {
UserPoolId: event.userPoolId,
UserAttributes: [
{
Name: 'phone_number_verified',
Value: 'true',
},
],
Username: event.userName,
};
await cognitoIdentityServiceProvider.adminUpdateUserAttributes(params).promise();
}
// if account created with email
if ('cognito:email_alias' in event.request.userAttributes) {
const params: CognitoIdentityServiceProvider.AdminUpdateUserAttributesRequest = {
UserPoolId: event.userPoolId,
UserAttributes: [
{
Name: 'email_verified',
Value: 'true',
},
],
Username: event.userName,
};
await cognitoIdentityServiceProvider.adminUpdateUserAttributes(params).promise();
}
}
callback(null, event);
};
import { Callback, CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler, Context } from 'aws-lambda';
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, context: Context, callback: Callback<CognitoUserPoolTriggerEvent>) => {
event.response.autoConfirmUser = true;
callback(null, event);
};
{
"version": "1",
"region": "us-east-1",
"userPoolId": "us-east-XXXX",
"userName": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "XXXXXXXXXXXXXXXXXXXXXXXXX"
},
"triggerSource": "PreSignUp_SignUp",
"request": {
"userAttributes": {
"phone_number": "+861234567890"
},
"validationData": null
},
"response": {
"autoConfirmUser": false,
"autoVerifyEmail": false,
"autoVerifyPhone": false
}
}
import { CognitoUserPoolTriggerEvent, CognitoUserPoolTriggerHandler, Context } from 'aws-lambda';
export const handler: CognitoUserPoolTriggerHandler = async (event: CognitoUserPoolTriggerEvent, context: Context) => {
event.response.answerCorrect = false;
if (event.request.privateChallengeParameters) {
const expectedAnswer = event.request.privateChallengeParameters.secretLoginCode;
if (event.request.challengeAnswer === expectedAnswer) {
event.response.answerCorrect = true;
}
}
context.done(undefined, event);
};
{
"version": "1",
"region": "us-east-1",
"userPoolId": "us-east-XXXX",
"userName": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"callerContext": {
"awsSdkVersion": "aws-sdk-unknown-unknown",
"clientId": "XXXXXXXXXXXXXXXXXXXXXXXXX"
},
"triggerSource": "VerifyAuthChallengeResponse_Authentication",
"request": {
"userAttributes": {
"sub": "XXXXX-XXXX-XXX-XXX-XXXXXXXXXXXXX",
"cognito:user_status": "CONFIRMED",
"phone_number_verified": "false",
"cognito:phone_number_alias": "+861234567890",
"phone_number": "+861234567890"
},
"privateChallengeParameters": {
"secretLoginCode": "123456"
},
"challengeAnswer": "123456"
},
"response": {
"answerCorrect": null
}
}
{
"email.subject": "Code de confirmation Example :",
"email.title": "Votre code de confirmation",
"email.subTitle1": "Merci de d'utiliser Example. Nous sommes ravis de vous compter parmi nous !",
"email.subTitle2": "Saisissez le code suivant dans la fenêtre où vous avez commencé à vous connecter à votre ville :",
"email.disclaimer": "Attention, cet e-mail contient des informations confidentielles sur votre compte Example. Ne le transférez à personne. Vous avez des questions sur la configuration de Example ? Envoyez-nous un e-mail à l’adresse",
"email.footerCreatedBy": "Conçu par",
"email.footerCreatedByCompany": "Example",
"sms.subject": "Code de confirmation Example :"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment