Lambda NodeJS works intermittent with async method - node.js

I have a lambda function written in node.js. The lambda logic is to extract secrets from aws secret manager and pass the params to another module with a token to make a soap call.This lambda was working as expected without async method to get secreats when secreats hardcoded.
But after adding an async getsecrets method the lambda works intermittently while testing in aws console. The getsecreats returns params every time. But the lambda terminates without the intended output. It doesn't make any soap call.
The logic of the lambda code is
Get the secret
Pass the secret to the CallServicewithToken()
get XML data from soap and populate it into the database.
Why it works intermittently when introducing async calls? I have tried introducing async to all methods. still the same issue. Appreciate any input/help.
The code is as below
'use strict';
var soap = require('strong-soap').soap;
const aws = require("aws-sdk");
const request = require('request');
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0;
var rfcUrl = process.env.rfcser_url;
var testUrl = process.env.faccser_url;
var region = process.env.region;
var secretName = process.env.secretName;
var oauthurl = process.env.oauthurl;
var rfcRequestArgs;
var parsedResult;
var tokenrequest;
exports.handler = async function(event, context,callback) {
const secret = await getSecrets();
CallServicewithToken();
};
async function getSecrets() {
const config = { region : region, apiVersion: 'latest' };
let secretManager = new aws.SecretsManager(config);
const Result = await secretManager.getSecretValue({ SecretId: secretName }).promise();
parsedResult = JSON.parse(Result.SecretString);
tokenrequest = {
url: oauthurl,
form: {
client_id: parsedResult.client_id,
client_secret: parsedResult.client_secret,
grant_type:'client_credentials',
scope:parsedResult.scope
}
};
console.log('client_id: ' + parsedResult.client_id);
console.log('client_secret: ' + parsedResult.client_secret);
console.log('testservice_userid: ' + parsedResult.testservice_userid);
}
function CallServicewithToken() {
console.log('Calling CallServicewithToken ');
request.post(tokenrequest, (err, res, body) => {
if (err) {
console.log(' error2: ' + err);
return;
}
rfcRequestArgs = {
UserName: parsedResult.service_username
};
var tokenobj = JSON.parse(body);
var token = 'Bearer '+ tokenobj.access_token;
var credentials = {
Authorization:{
AuthToken: token
}
}
var options = {};
console.log('Calling Service.');
soap.createClient(rfcUrl, options, function(err, client) {
client.addSoapHeader(
`<aut:Authorization xmlns:aut="http://soap.xyznet.net">
<aut:AuthToken>${token}</aut:AuthToken>
</aut:Authorization>`
);
var method = client['GetSourceLocationData'];
method(RequestArgs, function(err, result, envelope, soapHeader) {
if (err) {
console.log('error3: ' + err);
return;
}
else
{
console.log('Received response from GetSourceLocationData().');
CallTESTService(JSON.stringify(result));
}
});
function CallTESTService(LocData)
{
var testRequestArgs = {
UserID: parsedResult.testservice_userid,
AuthorizationKey: parsedResult.testservice_authorizationkey,
LocationData: LocData
};
console.log('Calling test Service.');
options = {};
soap.createClient(testUrl, options, function(err, client) {
client.addSoapHeader(
`<Authorization xmlns="testWebService">
<AuthToken>${token}</AuthToken>
</Authorization>`
);
var test_method = client['UpdateLocationData'];
console.log('Called UpdateLocationData service method.');
test_method(testRequestArgs, function(err, result, envelope, soapHeader) {
if(err) {
console.log('test error: ' + err);
return;
}
else
{
console.log('Response: \n' + JSON.stringify(result));
console.log('Data updated through test service method.');
}
});
});
}
});
});
}

Related

getting 403 from lambda calling api gateway

I have an api post end point which would update a customer's information in dynamodb. It is set to authenticate using AWS_IAM. I am getting 403 from my lambda when calling this api. I have allowed execute-api:Invoke permission to the api for the role lambda uses. I see in this post that I need to create a canonical request. I was able to come up with the below code and I still get a 403. I can't figure out what is missing and wish if a different eye can spot the problem. Please help!
"use strict";
const https = require("https");
const crypto = require("crypto");
exports.handler = async (event, context, callback) => {
try {
var attributes = {
customerId: 1,
body: { firstName: "abc", lastName: "xyz" }
};
await updateUsingApi(attributes.customerId, attributes.body)
.then((result) => {
var jsonResult = JSON.parse(result);
if (jsonResult.statusCode === 200) {
callback(null, {
statusCode: jsonResult.statusCode,
statusMessage: "Attributes saved successfully!"
});
} else {
callback(null, jsonResult);
}
})
.catch((err) => {
console.log("error: ", err);
callback(null, err);
});
} catch (error) {
console.error("error: ", error);
callback(null, error);
}
};
function sign(key, message) {
return crypto.createHmac("sha256", key).update(message).digest();
}
function getSignatureKey(key, dateStamp, regionName, serviceName) {
var kDate = sign("AWS4" + key, dateStamp);
var kRegion = sign(kDate, regionName);
var kService = sign(kRegion, serviceName);
var kSigning = sign(kService, "aws4_request");
return kSigning;
}
function updateUsingApi(customerId, newAttributes) {
var request = {
partitionKey: `MY_CUSTOM_PREFIX_${customerId}`,
sortKey: customerId,
payLoad: newAttributes
};
var data = JSON.stringify(request);
var apiHost = new URL(process.env.REST_API_INVOKE_URL).hostname;
var apiMethod = "POST";
var path = `/stage/postEndPoint`;
var { amzdate, authorization, contentType } = getHeaders(host, method, path);
const options = {
host: host,
path: path,
method: method,
headers: {
"X-Amz-Date": amzdate,
Authorization: authorization,
"Content-Type": contentType,
"Content-Length": data.length
}
};
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
if (res && res.statusCode !== 200) {
console.log("response from api", res);
}
var response = {
statusCode: res.statusCode,
statusMessage: res.statusMessage
};
resolve(JSON.stringify(response));
});
req.on("error", (e) => {
console.log("error", e);
reject(e.message);
});
req.write(data);
req.end();
});
}
function getHeaders(host, method, path) {
var algorithm = "AWS4-HMAC-SHA256";
var region = "us-east-1";
var serviceName = "execute-api";
var secretKey = process.env.AWS_SECRET_ACCESS_KEY;
var accessKey = process.env.AWS_ACCESS_KEY_ID;
var contentType = "application/x-amz-json-1.0";
var now = new Date();
var amzdate = now
.toJSON()
.replace(/[-:]/g, "")
.replace(/\.[0-9]*/, "");
var datestamp = now.toJSON().replace(/-/g, "").replace(/T.*/, "");
var canonicalHeaders = `content-type:${contentType}\nhost:${host}\nx-amz-date:${amzdate}\n`;
var signedHeaders = "content-type;host;x-amz-date";
var payloadHash = crypto.createHash("sha256").update("").digest("hex");
var canonicalRequest = [
method,
path,
canonicalHeaders,
signedHeaders,
payloadHash
].join("/n");
var credentialScope = [datestamp, region, serviceName, "aws4_request"].join(
"/"
);
const sha56 = crypto
.createHash("sha256")
.update(canonicalRequest)
.digest("hex");
var stringToSign = [algorithm, amzdate, credentialScope, sha56].join("\n");
var signingKey = getSignatureKey(secretKey, datestamp, region, serviceName);
var signature = crypto
.createHmac("sha256", signingKey)
.update(stringToSign)
.digest("hex");
var authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return { amzdate, authorization, contentType };
}

Basic HTTP Authentication for CloudFront with Lambda#Edge in NodeJS

I am working on protecting a static website with a username and password. I created a basic HTTP Authentication for CloudFront with Lambda#Edge in NodeJS.
I am completely new to NodeJS. Initially, I had the user and the password hardcoded, and this worked properly.
'use strict';
exports.handler = (event, context, callback) => {
// Get request and request headers
const request = event.Records[0].cf.request;
const headers = request.headers;
// Configure authentication
const authUser = 'user';
const authPass = 'pass';
// Construct the Basic Auth string
const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');
// Require Basic authentication
if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
const body = 'Unauthorized';
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: body,
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
},
};
callback(null, response);
}
// Continue request processing if authentication passed
callback(null, request);
};
I stored my secrets in SSM and I want to retrieve them through the function. I tested this piece of code separately in Lambda and it returns the credentials as espected.
'use strict';
exports.handler = async (event, context, callback) => {
const ssm = new (require('aws-sdk/clients/ssm'))();
let userData = await ssm.getParameters({Names: ['website-user']}).promise();
let userPass = await ssm.getParameters({Names: ['website-pass']}).promise();
let user = userData.Parameters[0].Value;
let pass = userPass.Parameters[0].Value;
return {user, pass};
};
But when I stitch the two, I get 503 ERROR The request could not be satisfied.
Does anyone know what I might be doing wrong? Thank you for your help!
The complete code:
'use strict';
exports.handler = async (event, context, callback) => {
const ssm = new (require('aws-sdk/clients/ssm'))();
let userData = await ssm.getParameters({Names: ['website-user']}).promise();
let userPass = await ssm.getParameters({Names: ['website-pass']}).promise();
let user = userData.Parameters[0].Value;
let pass = userPass.Parameters[0].Value;
// Get request and request headers
const request = event.Records[0].cf.request;
const headers = request.headers;
// Construct the Basic Auth string
let authString = 'Basic ' + new Buffer(user + ':' + pass).toString('base64');
// Require Basic authentication
if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
const body = 'Unauthorized';
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: body,
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
},
};
callback(null, response);
}
// Continue request processing if authentication passed
callback(null, request);
};
After reading about promises I was able to resolve the error. Here's the solution that worked for me:
'use strict';
var AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });
var ssm = new AWS.SSM();
function getParameter(param) {
return new Promise(function (success, reject) {
ssm.getParameter(param, function (err, data) {
if (err) {
reject(err);
} else {
success(data);
}
});
});
};
exports.handler = (event, context, callback) => {
let request = event.Records[0].cf.request;
let headers = request.headers;
let user = {Name: 'user-path', WithDecryption: false};
let pass = {Name: 'password-path', WithDecryption: false};
let authUser;
let authPass;
var promises = [];
promises.push(getParameter(user), getParameter(pass));
Promise.all(promises)
.then(function (result) {
authUser = result[0].Parameter.Value;
authPass = result[1].Parameter.Value;
console.log(authUser);
console.log(authPass);
let authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');
if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
const body = 'Unauthorized';
const response = {
status: '401',
statusDescription: 'Unauthorized',
body: body,
headers: {
'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
},
};
callback(null, response);
}
// Continue request processing if authentication passed
callback(null, request);
})
.catch(function (err) {
console.log(err);
});
};
Thank you for sharing the solution. And let me do my humble contribution.
To successfully authenticate additionally need the choice right version of Node.js(14.x)
Grant AWS Lambda Access to SSM Parameter Store to retrieve AWS Parameter Store:
Go to Lambda function -> Configuration -> Permissions -> Role name -> Whatever-Your-Role-Name-> Add Permissions -> Create inline Policy -> JSON -> And input the policy as shown below:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath",
"ssm:PutParameter"
],
"Resource": [
"arn:aws:ssm:us-east-1:YOUR_ACCOUNT_NUMBER:PARAMETER_NAME_WITHOUT_LEADING_SLASH",
"arn:aws:ssm:us-east-1:252522211181:parameter/web-pass"
]
}
]
}
You could set "ssm:*" for the Action element in the policy to grant full parameter store access to the lambda function.
You could also set the Resource element to be *, which means the function can access all SSM parameters in the account.

NodeJS API to upload image to S3 not returning response

I am trying to upload image to AWS S3 bucket using NodeJS. The issue I am facing it is while the image is getting saved but the API is returning 404(Not Found). Here is my controller code:
async UploadProfileImage(ctx) {
try {
var file = ctx.request.files.profileImage;
if (file) {
fs.readFile(file.path, (err, fileData) => {
var resp = s3Utility.UploadProfileImageToS3(file.name, fileData);
//Not reaching here. Although E3 tag printing in console.
console.log(resp);
ctx.status = 200;
ctx.body = { response: 'file Uploaded!' };
});
}
else {
ctx.status = 400;
ctx.body = { response: 'File not found!' };
}
} catch (error) {
ctx.status = 500;
ctx.body = { response: 'There was an error. Please try again later!' };
}
}
Utility Class I am using is:
const AWS = require('aws-sdk');
const crypto = require("crypto");
var fs = require('fs');
const mime = require('mime-types');
export class S3Utility {
constructor() { }
async UploadProfileImageToS3(fileName, data) {
let randomId = crypto.randomBytes(16).toString("hex");
AWS.config.update({ region: "Region", accessKeyId: "KeyID", secretAccessKey: "SecretAccessKey" });
var s3 = new AWS.S3();
var imageName = randomId + fileName;
var params = {
Bucket: "BucketName"
Key: imageName,
Body: data,
ContentType: mime.lookup(fileName)
};
return new Promise((resolve, reject) => {
s3.putObject(params, function (err, data) {
if (err) {
console.log('Error: ', err);
reject(new Error(err.message));
} else {
console.log(data);
resolve({
response: data,
uploadedFileName: imageName
});
}
});
});
}
}
const s3Utility: S3Utility = new S3Utility();
export default s3Utility;
The code is uploading file on S3 but it is not returning proper response. Upon testing this endpoint on postman, I get "Not Found" message. However, I can see E Tag getting logged in console. I don't know what is going wrong here. I am sure it has something to do with promise. Can someone please point out or fix the mistake?
Edit:
Using async fs.readFile does the trick.
const fs = require('fs').promises;
const fileData = await fs.readFile(file.path, "binary");
var resp = await s3Utility.UploadProfileImageToS3(file.name, fileData);

NodeJs unable to callback AWS Secrets Manager response

I'm trying to use AWS Secrets Manager to fetch my RDS credentials,
The Secrets Manager SDK is able to get the Secret properly,
But I am unable to export it back to my calling file.
I have 2 files -
1. index.js -
var mysql = require('mysql');
var secretsManager = require('./secrets-manager');
exports.handler = (event, context, callback) => {
secretsManager.getDbCredentialFromSecretsManager(function(err,creds) {
if (err) {
console.log(err);
callback(err, null);
}
else{
console.log("Creds ", creds);
var connection = mysql.createConnection(creds);
connection.connect(function(err) {
if (err) {
console.error(err.stack);
callback(err,null);
}
else{
callback(null,connection);
}
});
}
});
}
2. secrets-manager.js -
var AWS = require('aws-sdk');
var constants = require('/opt/nodejs/utils/constants');
module.exports = {
getRDSCredsFromSM
};
function getRDSCredsFromSM (callback) {
var response = {};
let secretName = "secretId";
var client = new AWS.SecretsManager({
region: constants.aws.region
});
client.getSecretValue({SecretId: secretName}, function(err, data) {
if (err) {
console.log(err);
callback(err, null);
}
else {
if ('SecretString' in data) {
let secret = data.SecretString;
secret = JSON.parse(secret);
console.log("secret",secret);
callback(null, secret);
} else {
let buff = new Buffer(data.SecretBinary, 'base64');
let decodedBinarySecret = buff.toString('ascii');
callback(null, decodedBinarySecret);
}
}
});
}
I feel there's some mistake from me on Node side,
Which is why the callback isn't working properly,
The Lambda Timesout,
And the logs show nothing in creds variable -
console.log("Creds ", creds);
Working code -
let async = require('async');
let AWS = require('aws-sdk');
module.exports = {
getDbCredentialFromSecretsManager
};
const TAG = '[SECRETS-MANAGER-UTIL->';
function getDbCredentialFromSecretsManager (constants, callback) {
let response = {};
const METHOD_TAG = TAG + 'getDbCredentialFromSecretsManager->';
async.waterfall([
function(callback) {
let client = new AWS.SecretsManager({
region: constants.aws.region
});
client.getSecretValue({SecretId: constants.aws.sm}, function(err, data) {
if (err) {
console.log(METHOD_TAG,err);
callback(err, null);
}
else {
console.log(METHOD_TAG, 'Secrets Manager call successful');
if ('SecretString' in data) {
let secret = data.SecretString;
secret = JSON.parse(secret);
response.user = secret.username;
response.password = secret.password;
response.host = secret.host;
response.database = constants.db.database;
callback(null, response);
} else {
let buff = new Buffer(data.SecretBinary, 'base64');
let decodedBinarySecret = buff.toString('ascii');
callback(null, decodedBinarySecret);
}
}
});
}
],
function(err, response) {
if (err) {
console.log(METHOD_TAG, err);
callback(err, response);
}
else {
callback(null, response);
}
});
}

How to properly call AWS.CloudFront.Signer in Node.js?

I tried to get a signed URL in a node.js app, my code is shown below.
var AWS = require('aws-sdk');
const fs = require("fs");
var options = { keypairId: 'keypairId', privateKeyPath: 'privateKeyPath', expireTime: (new Date().getTime() + 3000) };
var url = 'cloudfrontURL' + objectpath;
const key = fs.readFileSync('privateKeyPath').toString("ascii");
const id = 'keypairId';
const signer = new AWS.CloudFront.Signer(id, key);
const params = {
url: url,
expires: 1538999532,
};
signer.getSignedUrl(params, function (err, data) {
if (err) { console.log(err) }
console.log(data);
});
but I am getting an error, which is shown below
AWS.CloudFront.Signer is not a constructor
What is the reason for this?

Resources