Last active
February 8, 2020 12:08
-
-
Save vdelacou/1dccafd9516b13c4079df0caa44c5c70 to your computer and use it in GitHub Desktop.
Passwordless Cognito With email or Phone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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" | |
} | |
] | |
] | |
} | |
] | |
} | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
`; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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