Cognito send confirmation email using custom email - amazon-web-services

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);
}
}

Related

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

Using AWS Lambda Console to send push using SNS

I tried every possible solution on the internet with no hope
What I am trying to do is simply use aws lambda functions (through the aws console) to fetch user fcm token from lets say DynamoDB (not included in the question), use that token to create endpointArn, send push to that specific device
I tested to send Using SNS console and the push gets to the device successfully but I failed to get it to the device using Lambda functions although it gives success status and message ID
Here is the code I used
// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set region
AWS.config.update({region: 'us-east-1'});
const sns = new AWS.SNS()
const sampleMessage = {
"GCM": {
"notification": {
"body": "Sample message for Android endpoints",
"title":"Title Test"
}
}
}
exports.handler = async (event) => {
const snsPayload = JSON.stringify(sampleMessage);
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
const params = {
PlatformApplicationArn: '<Platform Arn>',
Token: '<FCM Token>'
};
try {
const endpointData = await sns.createPlatformEndpoint(params).promise();
const paramsMessage = {
Message: snsPayload,
TargetArn: endpointData.EndpointArn
};
var publishTextPromise = await sns.publish(paramsMessage).promise();
response.MessageId = publishTextPromise.MessageId;
response.result = 'Success';
}
catch (e) {
console.log(e.stack)
response.result = 'Error'
}
return response;
};
After some trials and errors I figured out the solution for my own question
1- The GCM part of the payload should be a string not a json
2- The message parameter should have an attribute that explicitly sets the mime type of the payload to Json
Taking all that into consideration
const GCM_data = {
'notification': {
'body': 'Hellow from lambda function',
'title': 'Notification Title'
}
}
const data = {
"GCM": JSON.stringify(GCM_data)
}
const snsPayload = JSON.stringify(data)
and the params should look like
const paramsMessage = {
Message: snsPayload,
TargetArn: endpointData.EndpointArn,
MessageStructure: 'json'
};
and this will work :)

Prevent users from signing up on their own with federated identity providers (FIP) but allow sign in with a FIP if added by an administrator

I've set up a user pool in Amazon Cognito for my web application. The application is not meant to be public and only specific users are allowed to sign in. The policies of that user pool in the Amazon Console allow only administrators to create new users.
I've implemented sign in through Facebook and Google. Cognito does indeed let users sign into the application with these federated identity providers, which is great. However, it seems that anybody with a Facebook or Google account can sign themselves up now.
So, on one hand, people can not create their own user with regular Cognito credentials but, on the other hand, they can create a new user in Cognito if they use a federated identity provider.
Is there a way to restrict signing into my application with Facebook or Google to only users that already exist in the user pool? That way, administrators would still be able to control who exactly can access the application. I would like to use the email shared by the federated identity provider to check if they are allowed to sign in.
The application is set up with CloudFront. I've written a Lambda that intercepts origin requests to check for tokens in cookies and authorize access based on the validity of the access token.
I would like to avoid writing additional code to prevent users to sign themselves up with Facebook or Google but if there is no other way, I'll update the Lambda.
So, here is the pre sign-up Lambda trigger I ended up writing. I took the time to use async/await instead of Promises. It works nicely, except that there is a documented bug where Cognito forces users who use external identity providers for the first time to sign up and then sign in again (so they see the auth page twice) before they can access the application. I have an idea on how to fix this but in the meantime the Lambda below does what I wanted. Also, it turns out that the ID that comes from Login With Amazon is not using the correct case, so I had to re-format that ID by hand, which is unfortunate. Makes me feel like the implementation of the triggers for Cognito is a bit buggy.
const PROVIDER_MAP = new Map([
['facebook', 'Facebook'],
['google', 'Google'],
['loginwithamazon', 'LoginWithAmazon'],
['signinwithapple', 'SignInWithApple']
]);
async function getFirstCognitoUserWithSameEmail(event) {
const { region, userPoolId, request } = event;
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({
region
});
const parameters = {
UserPoolId: userPoolId,
AttributesToGet: ['sub', 'email'], // We don't really need these attributes
Filter: `email = "${request.userAttributes.email}"` // Unfortunately, only one filter can be applied at once
};
const listUserQuery = await cognito.listUsers(parameters).promise();
if (!listUserQuery || !listUserQuery.Users) {
return { error: 'Could not get list of users.' };
}
const { Users: users } = listUserQuery;
const cognitoUsers = users.filter(
user => user.UserStatus !== 'EXTERNAL_PROVIDER' && user.Enabled
);
if (cognitoUsers.length === 0) {
console.log('No existing enabled Cognito user with same email address found.');
return {
error: 'User is not allowed to sign up.'
};
}
if (cognitoUsers.length > 1) {
cognitoUsers.sort((a, b) =>
a.UserCreateDate > b.UserCreateDate ? 1 : -1
);
}
console.log(
`Found ${cognitoUsers.length} enabled Cognito user(s) with same email address.`
);
return { user: cognitoUsers[0], error: null };
}
// Only external users get linked with Cognito users by design
async function linkExternalUserToCognitoUser(event, existingUsername) {
const { userName, region, userPoolId } = event;
const [
externalIdentityProviderName,
externalIdentityUserId
] = userName.split('_');
if (!externalIdentityProviderName || !externalIdentityUserId) {
console.error(
'Invalid identity provider name or external user ID. Should look like facebook_123456789.'
);
return { error: 'Invalid external user data.' };
}
const providerName = PROVIDER_MAP.get(externalIdentityProviderName);
let userId = externalIdentityUserId;
if (providerName === PROVIDER_MAP.get('loginwithamazon')) {
// Amazon IDs look like amzn1.account.ABC123DEF456
const [part1, part2, amazonId] = userId.split('.');
const upperCaseAmazonId = amazonId.toUpperCase();
userId = `${part1}.${part2}.${upperCaseAmazonId}`;
}
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({
region
});
console.log(`Linking ${userName} (ID: ${userId}).`);
const parameters = {
// Existing user in the user pool to be linked to the external identity provider user account.
DestinationUser: {
ProviderAttributeValue: existingUsername,
ProviderName: 'Cognito'
},
// An external identity provider account for a user who does not currently exist yet in the user pool.
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: userId,
ProviderName: providerName // Facebook, Google, Login with Amazon, Sign in with Apple
},
UserPoolId: userPoolId
};
// See https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminLinkProviderForUser.html
await cognito.adminLinkProviderForUser(parameters).promise();
console.log('Successfully linked external identity to user.');
// TODO: Update the user created for the external identity and update the "email verified" flag to true. This should take care of the bug where users have to sign in twice when they sign up with an identity provider for the first time to access the website.
// Bug is documented here: https://forums.aws.amazon.com/thread.jspa?threadID=267154&start=25&tstart=0
return { error: null };
}
module.exports = async (event, context, callback) => {
// See event structure at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
const { triggerSource } = event;
switch (triggerSource) {
default: {
return callback(null, event);
}
case 'PreSignUp_ExternalProvider': {
try {
const {
user,
error: getUserError
} = await getFirstCognitoUserWithSameEmail(event);
if (getUserError) {
console.error(getUserError);
return callback(getUserError, null);
}
const {
error: linkUserError
} = await linkExternalUserToCognitoUser(event, user.Username);
if (linkUserError) {
console.error(linkUserError);
return callback(linkUserError, null);
}
return callback(null, event);
} catch (error) {
const errorMessage =
'An error occurred while signing up user from an external identity provider.';
console.error(errorMessage, error);
return callback(errorMessage, null);
}
}
}
};
There is a way to do this but you will need to write some code - there is no out-of-the-box solution.
You will need to write a lambda and connect it to the Cognito Pre-Signup trigger.
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-sign-up.html
The trigger has three different sources of event; PreSignUp_SignUp, PreSignUp_AdminCreateUser and PreSignUp_ExternalProvider.
Your lambda should check you have the PreSignUp_ExternalProvider event. For these events, use the Cognito SDK to look the user up in your existing pool. If the user exists, return the event. If the user does not exist, return a string (error message).
I will paste my own Pre-Signup trigger here. It does not do what you need it to, but all the main components you need are there. You can basically hack it into doing what you require.
const AWS = require("aws-sdk");
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = (event, context, callback) => {
function checkForExistingUsers(event, linkToExistingUser) {
console.log("Executing checkForExistingUsers");
var params = {
UserPoolId: event.userPoolId,
AttributesToGet: ['sub', 'email'],
Filter: "email = \"" + event.request.userAttributes.email + "\""
};
return new Promise((resolve, reject) =>
cognito.listUsers(params, (err, result) => {
if (err) {
reject(err);
return;
}
if (result && result.Users && result.Users[0] && result.Users[0].Username && linkToExistingUser) {
console.log("Found existing users: ", result.Users);
if (result.Users.length > 1){
result.Users.sort((a, b) => (a.UserCreateDate > b.UserCreateDate) ? 1 : -1);
console.log("Found more than one existing users. Ordered by createdDate: ", result.Users);
}
linkUser(result.Users[0].Username, event).then(result => {
resolve(result);
})
.catch(error => {
reject(err);
return;
});
} else {
resolve(result);
}
})
);
}
function linkUser(sub, event) {
console.log("Linking user accounts with target sub: " + sub + "and event: ", event);
//By default, assume the existing account is a Cognito username/password
var destinationProvider = "Cognito";
var destinationSub = sub;
//If the existing user is in fact an external user (Xero etc), override the the provider
if (sub.includes("_")) {
destinationProvider = sub.split("_")[0];
destinationSub = sub.split("_")[1];
}
var params = {
DestinationUser: {
ProviderAttributeValue: destinationSub,
ProviderName: destinationProvider
},
SourceUser: {
ProviderAttributeName: 'Cognito_Subject',
ProviderAttributeValue: event.userName.split("_")[1],
ProviderName: event.userName.split("_")[0]
},
UserPoolId: event.userPoolId
};
console.log("Parameters for adminLinkProviderForUser: ", params);
return new Promise((resolve, reject) =>
cognito.adminLinkProviderForUser(params, (err, result) => {
if (err) {
console.log("Error encountered whilst linking users: ", err);
reject(err);
return;
}
console.log("Successfully linked users.");
resolve(result);
})
);
}
console.log(JSON.stringify(event));
if (event.triggerSource == "PreSignUp_SignUp" || event.triggerSource == "PreSignUp_AdminCreateUser") {
checkForExistingUsers(event, false).then(result => {
if (result != null && result.Users != null && result.Users[0] != null) {
console.log("Found at least one existing account with that email address: ", result);
console.log("Rejecting sign-up");
//prevent sign-up
callback("An external provider account alreadys exists for that email address", null);
} else {
//proceed with sign-up
callback(null, event);
}
})
.catch(error => {
console.log("Error checking for existing users: ", error);
//proceed with sign-up
callback(null, event);
});
}
if (event.triggerSource == "PreSignUp_ExternalProvider") {
checkForExistingUsers(event, true).then(result => {
console.log("Completed looking up users and linking them: ", result);
callback(null, event);
})
.catch(error => {
console.log("Error checking for existing users: ", error);
//proceed with sign-up
callback(null, event);
});
}
};

How to check after sending an email

I am using SES service of AWS for send email.
I use Python and boto3.
(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses.html#SES.Client.send_email)
I know how to check the overall statistics.
(https://aws.amazon.com/ko/premiumsupport/knowledge-center/ses-email-opens-clicks/)
But I don't know how to do individual inquiry.
After sending an email, message_id and request_id are returned.
I think we're going to use message_id, request_id to search.
I don't know what to do.
Configure SES to send notifications (send, deliver, bounce, open) to SNS topics.
Write a Lambda function to listen to the SNS topics. When a notification is received, write the event to your table (I use DynamoDB)
Now you have all your email events in a table
Here is the Lambda function I use for writing SES events to DynamoDB
const tableName = "DEV_EmailAuditLogs";
const AWS = require("aws-sdk");
AWS.config.update({ region: "us-east-1" });
const dynamodb = new AWS.DynamoDB({ apiVersion: "2012-10-08" });
exports.handler = (event, context, callback) => {
console.log(JSON.stringify(event, null, 2));
console.log("From SNS:", event.Records[0].Sns.Message);
console.log("Arn Topic:", event.Records[0].Sns.TopicArn);
const json = event.Records[0].Sns.Message;
const parsed = JSON.parse(json);
const type = parsed.eventType;
const sent = parsed.mail.timestamp.slice(0, -1);
let to, subject, from;
const headers = parsed.mail.headers;
for (const h of headers) {
switch (h.name) {
case "To":
to = h.value;
break;
case "From":
from = h.value;
break;
case "Subject":
subject = h.value;
break;
}
}
const eventData = parsed[type.toLowerCase()];
let created;
if (eventData && eventData.timestamp) {
created = eventData.timestamp.slice(0, -1);
} else {
created = sent;
}
var params = {
TableName: tableName,
Item: {
to: { S: to },
from: { S: from },
created: { S: created },
sent: { S: sent },
type: { S: type },
version: { N: "1" }
}
};
if (subject){
params.Item.subject = { S: subject };
}
dynamodb.putItem(params, function(err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
}
});
};

Cognito custom-message triggered lambda returns InvalidLambdaResponseException

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);
}
};