Cognito custom-message triggered lambda returns InvalidLambdaResponseException - amazon-web-services

I've created a lambda and assigned it to cognito throw the UI as its custom-message lambda.
Here is the code in typescript:
export const handler = async (event) => {
const trigger = event.triggerSource
const customMessage = cloneDeep(customMessages[trigger])
if (customMessage) {
try {
// inject cognito values to custom message
const codeParameter = event.request.codeParameter
const usernameParameter = event.request.usernameParameter
for (let key in customMessage) {
let text = customMessage[key]
if (codeParameter) {
customMessage[key] = text.replace(/{{codeParameter}}/g, codeParameter)
}
if (usernameParameter) {
customMessage[key] = text.replace(/{{usernameParameter}}/g, usernameParameter)
}
}
// load HTML template
let htmlFile = readFileSync(templateFilePath, { encoding: 'utf8' })
htmlFile = htmlFile.replace(/(\r\n|\n|\r)/gm, '')
const template = handlebars.compile(htmlFile)
const html = template(customMessage)
event.emailMessage = html
event.response.emailSubject = customMessage.title
} catch (err) {
logger.error(err)
return event
}
}
return event
}
Basically it loads an html template file and injects the code-parameters and username.
now the response our signup flow lambda returns is:
{
"message": "InvalidLambdaResponseException",
"details": "Unrecognizable lambda output"
}
I event tried to copy paste AWS example:
exports.handler = (event, context, callback) => {
//
if(event.userPoolId === "theSpecialUserPool") {
// Identify why was this function invoked
if(event.triggerSource === "CustomMessage_SignUp") {
// Ensure that your message contains event.request.codeParameter. This is the placeholder for code that will be sent
event.response.smsMessage = "Welcome to the service. Your confirmation code is " + event.request.codeParameter;
event.response.emailSubject = "Welcome to the service";
event.response.emailMessage = "Thank you for signing up. " + event.request.codeParameter + " is your verification code";
}
// Create custom message for other events
}
// Customize messages for other user pools
// Return to Amazon Cognito
callback(null, event);
};
The response is the same.
Any suggestions?
Thanks

Here is my custom message lambda. It runs on Node 8.10. Maybe you'd like to test/adapt it. I've stripped some other stuff out but it should work fine
exports.handler = function(event, context) {
const cognitoUserPool = 'us-east-1_AAAAAA';
const snsTopicArn = 'arn:aws:sns:us-east-1:9999999999:BBBBBBBBBB';
const baseurl = 'https://company.us-east-1.elasticbeanstalk.com/app';
console.log('Cognito Event:', event);
var AWS = require("aws-sdk");
if(event.userPoolId === cognitoUserPool) {
if(event.triggerSource === "CustomMessage_SignUp") {
event.response.emailSubject = "Welcome to Company";
event.response.emailMessage = "Hello etc";
context.done(null, event);
}
if(event.triggerSource === "CustomMessage_ResendCode") {
event.response.emailSubject = "Welcome to Company";
event.response.emailMessage = "Some other message etc";
context.done(null, event);
}
if(event.triggerSource === "CustomMessage_ForgotPassword") {
event.response.emailSubject = "Your password reset";
event.response.emailMessage = "Some other message again etc";
context.done(null, event);
}
// Other event types can go here
} else {
context.done(null, event);
}
};

Related

AWS: API gateway 502 error randomly with Runtime segmentation faults

I am using AWS and have an api which is called via API gateway which calls a node.js lambda function.
Very often but randomly I get 502 responses but when I immediately try again with the exact same request I get a normal response. So I decided to search the logs to see if I could find any issues.
The following is what I found for 1 of the requests:
RequestId: xxxxx Error: Runtime exited with error: signal: segmentation fault Runtime.ExitError
as well as:
xxxx ERROR Uncaught Exception
{
"errorType": "Error",
"errorMessage": "Quit inactivity timeout",
"code": "PROTOCOL_SEQUENCE_TIMEOUT",
"fatal": true,
"timeout": 30000,
"stack": [
"Error: Quit inactivity timeout",
" at Quit.<anonymous> (/opt/nodejs/node_modules/mysql/lib/protocol/Protocol.js:160:17)",
" at Quit.emit (node:events:527:28)",
" at Quit.emit (node:domain:475:12)",
" at Quit._onTimeout (/opt/nodejs/node_modules/mysql/lib/protocol/sequences/Sequence.js:124:8)",
" at Timer._onTimeout (/opt/nodejs/node_modules/mysql/lib/protocol/Timer.js:32:23)",
" at listOnTimeout (node:internal/timers:559:17)",
" at processTimers (node:internal/timers:502:7)"
]
}
the following is my reusable sql connector:
const CustomSecret = require('../secrets/CustomSecret');
const mysql = require("mysql");
module.exports = class MySqlConnect {
databaseCredObject;
constructor() {
}
async queryDb(sql, args) {
if (!this.databaseCredObject) {
await this.fetchSecret();
}
let connection = null;
const connection_settings = {
host: this.databaseCredObject.host,
user: this.databaseCredObject.username,
password: this.databaseCredObject.password,
database: 'logbook'
};
connection = mysql.createConnection(connection_settings);
return new Promise((resolve, reject) => {
connection.connect(function (err) {
if (err) {
console.log('error when connecting to db:', err);
} else {
console.log('Connected');
connection.query(sql, args, function (err, result) {
connection.end();
if (err) {
return reject(err);
}
return resolve(result);
});
}
});
});
}
async fetchSecret() {
const databaseCredString = await CustomSecret.getSecret('secretname', 'eu-west-2');
this.databaseCredObject = JSON.parse(databaseCredString);
}
}
Finally this is an example of my lambda function (shortened version):
const {compress, decompress} = require("compress-json");
const MySqlConnect = require("customPackagePath/MySqlConnect");
const CustomJwt = require("customPackagePath/CustomJwt");
const AWS = require("aws-sdk");
const warmer = require("lambda-warmer");
exports.handler = async (event) => {
if (await warmer(event)) {
console.log("Warming");
return 'warmed';
}
let responseCode = 200;
let response = {};
response.headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
};
const bodyContent = JSON.parse(event.body);
const dataType = bodyContent.dataType;
const webAuth = new CustomJwt();
const decodedToken = webAuth.decodeToken(event.headers.Authorization);
const userUUID = decodedToken['uuid'];
const connection = new MySqlConnect();
let sql;
switch (dataType) {
case 'userPreferences':
sql = await connection.queryDb('SELECT * FROM user WHERE uuid = ?', [userUUID]);
break;
}
let data = [];
for (let index in sql) {
data.push(JSON.parse(JSON.stringify(sql[index])));
}
const returnData = {
data
};
let compressed = compress(returnData);
response.statusCode = responseCode;
response.body = JSON.stringify(compressed);
return response;
};
Now I am new to infrastructure stuff. But it seems to me that once a lambda function has been called, its not closing or ending correctly. Also I am using the lambda warmer to keep the functions warm as seen in the lambda code and not sure if that is causing any issues.
Appreciate any help with this as I can't seem to figure it out.
Thanks
After doing more research I decided to add this to my Lambda function:
exports.handler = async (event, context, callback) => {
and the return like this
callback(null, response);
and ever since this issue seems to have been resolved. I am not entirely sure why but for now its looking good :)

Flutter aws amplify not returning data when calling graphql api

On button click I have programmed to call a graphql api which is connected to a Lambda function and the function is pulling data from a dynamodb table. The query does not produce any error, but it doesn't give me any results as well. I have also checked the cloudwatch logs and I dont see any traces of the function being called. Not sure on the careless mistake I am making here.
Here is my api
void findUser() async {
try {
String graphQLDocument = '''query getUserById(\$userId: ID!) {
getUserById(userId: \$id) {
id
name
}
}''';
var operation = Amplify.API.query(
request: GraphQLRequest<String>(
document: graphQLDocument,
variables: {'id': 'USER-14160000000'}));
var response = await operation.response;
var data = response.data;
print('Query result: ' + data);
} on ApiException catch (e) {
print('Query failed: $e');
}
}
Here is my lambda function -
const getUserById = require('./user-queries/getUserById');
exports.handler = async (event) => {
var userId = event.arguments.userId;
var name = event.arguments.name;
var avatarUrl = event.arguments.avatarUrl;
//console.log('Received Event - ', JSON.stringify(event,3));
console.log(userId);
switch(event.info.fieldName) {
case "getUserById":
return getUserById(userId);
}
};
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({region: 'ca-central-1'});
async function getUserById(userId) {
const params = {
TableName:"Bol-Table",
KeyConditionExpression: 'pk = :hashKey and sk = :sortKey',
ExpressionAttributeValues: {
':hashKey': userId,
':sortKey': 'USER'
}
};
try {
const Item = await docClient.query(params).promise();
console.log(Item);
return {
id: Item.Items[0].pk,
name: Item.Items[0].details.displayName,
avatarUrl: Item.Items[0].details.avatarUrl,
createdAt: Item.Items[0].details.createdAt,
updatedAt: Item.Items[0].details.updatedAt
};
} catch(err) {
console.log("BOL Error: ", err);
}
}
module.exports = getUserById;
Upon button click I get this
Moving my comment to an answer:
Can you try changing your graphQLDocumnet to
String graphQLDocument = '''query getUserById(\$id: ID!) {
getUserById(userId: \$id) {
id
name
}
}''';
Your variable is $userId and then $id. Try calling it $id in both places like in your variables object.
Your flutter code is working fine but in lambda from the aws is returning blank string "" to not to print anything

AWS Cognito: Custom Challenge with Retry

I am using Custom Challenge for MFA because i wanted to use Twilio instead of AMAZON SNS. i have successfully implemented it. It works fine but
When a user enters wrong OTP code. The user session is expired. means that he has to again provide phone number and request a OTP again. Whereas i want it to retry for atleast 3 time. before he need to request another OTP.
My Response verify trigger is as simple as below, is there something that we can do.
(event, context, callback) => {
if (event.request.privateChallengeParameters.answer == event.request.challengeAnswer) {
event.response.answerCorrect = true;
} else {
event.response.answerCorrect = false;
}
callback(null, event);
}
I acheived this by adding the answer as a variable into challengeMetaData - which so far as I can see is not returned to the client but is available on subsequent calls, I also have a variable named attempts to track how many times the user has entered an incorrect value.My code is below - I hope it helps
const AWS = require("aws-sdk");
exports.handler = (event, context, callback) => {
const session = event.request.session;
const currentSession = session ? session.length - 1 : 0
switch (event.triggerSource) {
case 'DefineAuthChallenge_Authentication':
console.log("DefineAuthChallenge_Authentication");
console.log(event);
if (session.length === 0) {
event.response = {
challengeName: 'CUSTOM_CHALLENGE',
failAuthentication: false,
issueTokens: false
};
}
else {
if (session[currentSession].challengeName === 'CUSTOM_CHALLENGE') {
if (session[currentSession].challengeResult === true) {
event.response.issueTokens = true;
event.response.failAuthentication = false;
}
else {
let metaData = JSON.parse(session[currentSession].challengeMetadata);
if (metaData.attempts <= 3) {
event.response = {
challengeName: 'CUSTOM_CHALLENGE',
failAuthentication: false,
issueTokens: false
};
}
else {
event.response.issueTokens = false;
event.response.failAuthentication = true;
}
}
}
}
console.log(event);
break;
case 'CreateAuthChallenge_Authentication':
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
console.log("CreateAuthChallenge_Authentication");
console.log(event);
if (session.length === 0) {
let answer = Math.random().toString(10).substr(2, 6);
//Your logic to send a message goes here
event.response.publicChallengeParameters = { challengeType: 'SMS_CODE' };
event.response.privateChallengeParameters = { answer: answer };
event.response.challengeMetadata = JSON.stringify({ '_sid': answer, 'challengeType': 'SMS_CODE', attempts: 1 });
}
else {
let metaData = JSON.parse(session[currentSession].challengeMetadata);
if (metaData.attempts <= 3) {
event.response.publicChallengeParameters = { challengeType: 'SMS_CODE', errorCode: 'NotAuthorizedException' };
event.response.privateChallengeParameters = { answer: metaData._sid };
event.response.challengeMetadata = JSON.stringify({ '_sid': metaData._sid, 'challengeType': 'SMS_CODE', attempts: metaData.attempts + 1 });
}
}
}
console.log(event);
break;
default:
console.log("VerifyAuthChallenge_Authentication");
console.log(event);
if (event.request.privateChallengeParameters.answer === event.request.challengeAnswer) {
event.response.answerCorrect = true;
}
else { event.response.answerCorrect = false; }
console.log(event);
break;
}
callback(null, event);
};

Amazon Lex intent

I have two intents:
This is the chat bot now:
After this there will be a confirmation whether he wants to invest in equity or not. If he says Yes then another intent must be started without him typing anything.
How do I achieve this?
Here is my Lambda function:
// --------------- Intents -----------------------
var type;
/**
* Called when the user specifies an intent for this skill.
*/
function dispatch(intentRequest, callback) {
// console.log(JSON.stringify(intentRequest, null, 2));
console.log(`dispatch userId=${intentRequest.userId}, intent=${intentRequest.currentIntent.name}`);
const name = intentRequest.currentIntent.name;
// Dispatch to your skill's intent handlers
if (name === 'FinancialType') {
return getFinancialType(intentRequest,callback);
}
throw new Error(`Intent with name ${name} not supported`);
}
// --------------- Main handler -----------------------
function loggingCallback(response, originalCallback) {
// console.log(JSON.stringify(response, null, 2));
originalCallback(null, response);
}
// Route the incoming request based on intent.
// The JSON body of the request is provided in the event slot.
exports.handler = (event, context, callback) => {
try {
// By default, treat the user request as coming from the America/New_York time zone.
process.env.TZ = 'America/New_York';
console.log(`event.bot.name=${event.bot.name}`);
/**
* Uncomment this if statement and populate with your Lex bot name and / or version as
* a sanity check to prevent invoking this Lambda function from an undesired Lex bot or
* bot version.
*/
/*
if (event.bot.name !== 'MakeAppointment') {
callback('Invalid Bot Name');
}
*/
dispatch(event, (response) => loggingCallback(response, callback));
} catch (err) {
callback(err);
}
};
function close(fulfillmentState, message) {
return {
dialogAction: {
type: 'Close',
fulfillmentState,
message,
},
};
}
function elicitSlot(intentName, slots, slotToElicit, message) {
return {
dialogAction: {
type: 'ElicitSlot',
intentName,
slots,
slotToElicit,
message,
},
};
}
function buildValidationResult(isValid, violatedSlot, messageContent) {
return {
isValid,
violatedSlot,
message: { contentType: 'PlainText', content: messageContent },
};
}
function getFinancialType(intentRequest,callback){
var age = intentRequest.currentIntent.slots.age;
var amount = intentRequest.currentIntent.slots.amount;
const source = intentRequest.invocationSource;
if(amount >= 10000){
type = 'Equity';
}
callback(close('Fulfilled',{contentType: 'PlainText',
content: `You have choosen to invest ` + amount + ' in ' + type }));
}
There is an option in the AWS Console for Lex to include a confirmation message. You can ask the user for confirmation there.
Documentation: http://docs.aws.amazon.com/lex/latest/dg/howitworks-manage-prompts.html#msg-prompts-context-for-msgs

Cognito send confirmation email using custom email

There's a way to send an email other than the one specified in the "Message customisation" tab on Cognito user pool?
I would like to use different email based on some parameters.
E.g.
verification#my-service.com for verification email
welcome#my-service.com for welcome email
You can go to the general settings in Cognito then click on triggers. There you can select Post Confirmation lambda function(this example in node) to send the email. In the lambda function you can make the subject whatever you like and change from email address.
var aws = require('aws-sdk');
var ses = new aws.SES();
exports.handler = function(event, context) {
console.log(event);
if (event.request.userAttributes.email) {
// Pull another attribute if you want
sendEmail(event.request.userAttributes.email,
"Congratulations "+event.userName+", you have been registered!"
, function(status) {
context.done(null, event);
});
} else {
// Nothing to do, the user's email ID is unknown
console.log("Failed");
context.done(null, event);
}
};
function sendEmail(to, body, completedCallback) {
var eParams = {
Destination: {
ToAddresses: [to]
},
Message: {
Body: {
Text: {
Data: body
}
},
Subject: {
Data: "Welcome to My Service!"
}
},
Source: "welcome#my-service.com"
};
var email = ses.sendEmail(eParams, function(err, data){
if (err) {
console.log(err);
} else {
console.log("===EMAIL SENT===");
}
completedCallback('Email sent');
});
console.log("EMAIL CODE END");
};
You will also have to set up SES.
If you want to handle all emails yourself, you can specify this with a CustomEmailSender Lambda. This trigger isn't currently available through the AWS Console, but you can specify it with the CLI or CDK/CloudFormation. See the docs here.
Those docs are pretty terrible though. The gist is that you'll be given a code property on the event, which is a base64-encoded blob that was encrypted with the KMS key you specified on your user pool. Depending on the triggering event, this is the verification code, temporary password, etc, generated by Cognito. Here's a simplified version of what my Lambda looks like:
import { buildClient, CommitmentPolicy, KmsKeyringNode } from '#aws-crypto/client-node';
const { decrypt } = buildClient(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT);
const kmsKeyring = new KmsKeyringNode({
keyIds: [process.env.COGNITO_EMAILER_KEY_ARN]
});
export async function lambdaHandler(event, context) {
try {
let payload = '';
if (event.request.code) {
const { plaintext, messageHeader } = await decrypt(
kmsKeyring,
Buffer.from(event.request.code, "base64")
);
if (event.userPoolId !== messageHeader.encryptionContext["userpool-id"]) {
console.error("Encryption context does not match expected values!");
return;
}
payload = plaintext.toString();
}
let messageHtml = "";
switch (event.triggerSource) {
case "CustomEmailSender_SignUp": {
const verificationCode = payload;
messageHtml = `<p>Use this code to verify your email: ${verificationCode}</p>`;
break;
}
case "CustomEmailSender_AdminCreateUser":
case "CustomEmailSender_ResendCode": {
const tempPassword = payload;
messageHtml = `<p>Your temporary password is ${tempPassword}</p>`;
break;
}
default: {
console.warn("unhandled trigger:", event.triggerSource);
return;
}
}
await sendEmail({
subject: "Automated message",
to: event.request.userAttributes.email,
messageHtml,
});
return true;
} catch (err) {
console.error(err.message);
process.exit(1);
}
}