AWS lambda basic-authentication without custom authorizer - node.js

I have a problem with setting up the basic authentication for my AWS lambda function written in Node.js.
The problem:
AWS lambda function which is a proxy for an additional service. This function only forwards the whole request and give the user the whole response. That's why I need to force the usage of the Authentication header and I would like to have the prompt window for passing the credentials: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
Apart from the proxy part of my lambda function, I focused on the problem with authentication and I have written this code:
export const proxy = async (event) => {
const authorizationHeader = event.headers.Authorization;
if (typeof authorizationHeader === undefined) {
throw new Error("Unauthorized");
}
...
};
service:
name: proxy-auth-test
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
memorySize: 128
timeout: 10
functions:
proxy-async:
handler: handler.proxy
events:
- http:
method: get
path: api/proxy
resources:
Resources:
GatewayResponse:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.WWW-Authenticate: "'Basic'"
ResponseType: UNAUTHORIZED
RestApiId:
Ref: 'ApiGatewayRestApi'
StatusCode: '401'
The endpoint is working properly, but I can't get the prompt window for passing the credentials. I set up the GatewayResponse according to this https://medium.com/#Da_vidgf/http-basic-auth-with-api-gateway-and-serverless-5ae14ad0a270 but I don't wanna provide the additional lambda function which is responsible only for authorization of the users.
In my case, I can't authorize the users before executing the final lambda function because this function only forwards the request (credentials too), nothing more.
Has anyone ever tried to setup basic auth with the prompt window without the additional authorizer with the usage of serverless and AWS lambda?

When returning a response from an integration the WWW-Authenticate is remapped to X-Amzn-Remapped-WWW-Authenticate (1). Browsers will not handle this remapped header so they don't show a prompt.
This means that you have to move your authorization logic to the Lambda Authorizer at a HTTP request level and return 'unauthorized' to the callback as stated in the medium link that you referenced. This is the only way to return a WWW-Authenticate header as of now.
Sources:
1: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html

You are not returning statusCode in the response. The article you are following seems to be using Custom Authorizer that always returns 401 status code with callback('Unauthorized'). Your Lambda function needs to return appropriate error code and headers.
if (typeof authorizationHeader === undefined) {
return {
statusCode: 401,
body: 'unauthorized',
headers: {
'WWW-Authenticate': 'Basic'
}
}
}

Related

aws lambda - 503 service unavailable when calling 3rd party API using axios

I have a lambda function which is calling 3rd party API using axios, when it calls 3rd party API, it creates a new entry on their database, which is working fine but the lambda function is returning 503 service unavailable
Following is my code -
let algcon = {
method: 'post',
url: constants.API_URL,
timeout: 1000 * 7,
headers: {
'Content-Type': 'application/json',
"User-Agent": "axios 0.21.1",
'token': myToken ? JSON.stringify(myToken.access) : ''
},
data: invoiceData,
};
await axios(algcon).then(function (response) {
}).catch(function (error) {
console.log(error) //here it is throwing 503 service unavailable error
});
I have increased lambda execution time but still getting the same error. Please help!!
Your code looks fine to me,
the default timeout of an API gateway response is 6500 milliseconds
You can update it as -
Go to API gateway
Select Lamda function
Click on integration
Click on Manage integration
Update default timeout
Like I did in below screenshot -

Amazon DynamoDB scan makes two HTTP requests to server provoking an error, why?

I've a typescript react application who's using DynamoDB services for storing data.
I've javascript middleware that manages the db access operations.
I developed locally the scan and put and delete operations with no errors.
Once I setup the production environment, I have an error.
If I execute the code locally with production environment I have the same error.
I noticed that scan operation fails.
Actually it fails in a weird way, because I saw that request to aws server is done twice.
To make my debug life simple I moved for debugging all db access within my react onClick method, in order to isolate the operation from lifecycle of the applicatio.
I noticed that
the onClick fetch operation is indeed called once (as expected)
the DynamoDB scan operation is indeed called once (as expected)
the calllback of the scann is executed twice (not as expected)
the first time data is indeed fetched and I can log it (perfect)
the second call is executed with an error
aws config code
const awsConfig = {
accessKeyId: process.env.REACT_APP_AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.REACT_APP_AWS_SECRET_ACCESS_KEY,
region: process.env.REACT_APP_AWS_REGION,
endpoint: process.env.REACT_APP_AWS_ENDPOINT
}
new AWS.Endpoint(awsConfig.endpoint || '')
AWS.config.update({
region: awsConfig.region,
//endpoint: awsConfig.endpoint || '',
credentials: new AWS.Credentials(awsConfig.accessKeyId || '', awsConfig.secretAccessKey || ''),
sslEnabled: false,
maxRetries: 5
})
const dynamoDbClient = new AWS.DynamoDB.DocumentClient()
note to this part of the code, Typescript gives me an error if I leave the endpoint in the AWS.config.update so I set it differently new AWS.Endpoint(awsConfig.endpoint || '')
Argument of type '{ region: string | undefined; endpoint: string; credentials: Credentials; sslEnabled: false; maxRetries: number; }' is not assignable to parameter of type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.
Object literal may only specify known properties, and 'endpoint' does not exist in type 'ConfigurationOptions & ConfigurationServicePlaceholders & APIVersions'.ts(2345)
aws access code
const searchParams = { TableName: 'table' }
let dataItems: DateFields[] = []
await dynamoDbClient
.scan(searchParams, (error: any, data: any) => {
if (error) {
console.error(`Error fetchCategories: `, JSON.stringify(error, null, 2))
return
}
dataItems = data.Items
})
.promise()
with the following error
Error fetchCategories: {
"message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.",
"code": "InvalidSignatureException",
"time": "2020-04-29T08:39:14.449Z",
"requestId": "xxxxxxxxxxxxxxxxxxx",
"statusCode": 400,
"retryable": false,
"retryDelay": 15.082420221534553
}
{"__type":"com.amazon.coral.service#InvalidSignatureException","message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
Any idea why this is happening? Thanks a lot
The issue lies in the aws access code and to fix it I updated the DynamoDB client like this:
try {
const data = await dynamoDbClient.scan(params).promise()
return data.Items
} catch(error) {
logError(operation, error)
}
This change in the code prevents:
the callback to be executed twice
the server to answer with an error that probably reflects an AWS security mechanism blocking multiple identical and consequential server query.

Get Microsoft GRAPH access token from Nodejs script

This question builds on How to get Microsoft Graph API Access token from Node Script?, however, as a first-time user of, I don't have the required reputation for commenting on the accepted answer in that thread.
The thing is, I tried to implement the approach suggested by the accepted answer, but somewhere it goes wrong. The below code is part of an async function, and I can already tell you that the ONEDRIVE_TENANT_URI is of the format XXX.onmicrosoft.com.
const endpoint = `https://login.microsoftonline.com/${process.env.ONEDRIVE_TENTANT_URI}/oauth2/token`;
const requestParams = {
grant_type: "client_credentials",
client_id: process.env.ONEDRIVE_APP_ID,
client_secret: process.env.ONEDRIVE_CLIENT_SECRET,
resource: "https://graph.windows.net"
};
const authResponse = await request.post({
url: endpoint,
form: requestParams
});
authResponse gets, as its body, just a string with the requestParams as defined above filled out.
If I submit the post request via Postman, with the same parameters as x-www-form-urlencoded, I DO get an access_token in the response body.
So... What do I do wrong? Maybe - but I don't think so - it's because this function is invoked by a (for testing purposes) POSTMAN GET request with a json-formatted body?
You can download the sample here. And fill in the credentials in config.js. You can find them from Azure portal.
This is the code to get access token.
auth.getAccessToken = function () {
var deferred = Q.defer();
// These are the parameters necessary for the OAuth 2.0 Client Credentials Grant Flow.
// For more information, see Service to Service Calls Using Client Credentials (https://msdn.microsoft.com/library/azure/dn645543.aspx).
var requestParams = {
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
resource: 'https://graph.microsoft.com'
};
// Make a request to the token issuing endpoint.
request.post({ url: config.tokenEndpoint, form: requestParams }, function (err, response, body) {
var parsedBody = JSON.parse(body);
console.log(parsedBody);
if (err) {
deferred.reject(err);
} else if (parsedBody.error) {
deferred.reject(parsedBody.error_description);
} else {
// If successful, return the access token.
deferred.resolve(parsedBody.access_token);
}
});
return deferred.promise;
};
You will get the access token successfully.
You've got two issues going on.
The first isn't an issue yet, but it will be once you try to call Microsoft Graph. The resource should be graph.microsoft.net, not graph.windows.net. The graph.windows.net refers to the legacy Azure AD Graph API, not Microsoft Graph.
The other issue, which is the root cause of this error, is await request.post. Request doesn't natively support promises. From the Request the documentation:
request supports both streaming and callback interfaces natively. If you'd like request to return a Promise instead, you can use an alternative interface wrapper for request. These wrappers can be useful if you prefer to work with Promises, or if you'd like to use async/await in ES2017.
Several alternative interfaces are provided by the request team, including:
request-promise (uses Bluebird Promises)
request-promise-native (uses native Promises)
request-promise-any (uses any-promise Promises)

How to allow CORS for custom headers in Serverless?

The core question here is: "how do I allow custom headers in a CORS GET request that is handled with the Serverless framework?". If you know the answer to that, pass Go, collect $200 and please answer that question. If it's not a question with a straight answer, here are the details:
I am writing an app using the Serverless framework on AWS Lambda (the API is managed through AWS API Gateway. Frankly, I'm not entirely sure what that means or what benefit that provides me but that's what Serverless automatically configured for me). I am attempting to create an open API which requires CORS to be enabled. I am using the Lambda Proxy integration. I have followed the practices found here. They have brought me partial success. My app currently has CORS enabled if I do not include my custom headers. However, it still does not work with custom headers.
When I send the following request to my API:
var data = null;
var xhr = new XMLHttpRequest();
xhr.withCredentials = false;
xhr.addEventListener("readystatechange", function () {
if (this.readyState === 4) {
console.log(this.responseText);
}
});
xhr.open("GET", "https://api.spongebobify.com/");
xhr.setRequestHeader("text", "hey");
xhr.send(data);
... I get this error:
Failed to load https://api.spongebobify.com/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://forum.serverless.com' is therefore not allowed access.
This error message is confirmed if I check the "response headers" using Chrome dev tools: there is no Access-Control-Allow-Origin in the response headers.
However, if I send the same request with the setRequestHeader() commented out, it works perfectly (yes, I know it returns a 403 error: that is intentional behavior).
Here's what I think is happening. My service has two potential CORS problems: domain related (a request not coming from the origin domain) and custom header related (a header not safe-listed by the CORS spec, more here). Somehow, the Serverless framework trips up on the second issue which causes it not even get to the point where it issues the appropriate headers to allow all ("*") domains.
Here is my serverless.yml config file:
# serverless.yml
service: spongebobify
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: us-east-1
functions:
app:
handler: handler.endpoint
events:
- http: GET /
cors:
origin: '*'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
- Startlower
- Text
- Access-Control-Allow-Headers
- Access-Control-Allow-Origin
allowCredentials: false
and here is the function that I am trying to run. You can see my many attempts to set the headers properly. I'm 60% convinced that a fix will come via the serverless.yml file at this point.
"use strict";
const spongebobify = require("spongebobify");
module.exports.endpoint = (event, context, callback) => {
let startLower = event.headers.startlower === "false" ? false : true;
try {
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*", // Required for CORS support to work
"Access-Control-Allow-Headers": "content-type,origin,text,startlower",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"content-type": "text/plain",
"Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
},
body: spongebobify(event.headers.text, startLower)
};
callback(null, response);
} catch (err) {
console.log(err);
const response = {
statusCode: 403,
headers: {
"Access-Control-Allow-Origin": "*", // Required for CORS support to work
"Access-Control-Allow-Headers": "content-type,origin,X-text,startlower",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"content-type": "text/plain",
"Access-Control-Allow-Credentials": true // Required for cookies, authorization headers with HTTPS
},
body: "Malformed request."
};
callback(null, response);
}
};
You can replicate my problem my running the above XMLHttpRequest in the dev console on the following sites:
api.spongebobify.com with the custom header enabled or disabled. It will work perfectly in both cases (because it won't be cross origin).
Any site that doesn't have a properly configured CSP with the custom header enabled. The OPTIONS request will fail and it will accurately report that there is no Access-Control-Allow-Origin header
Any site that doesn't have a properly configured CSP without the custom header enabled. The OPTIONS request will pass (which you'll know because Chrome will never tell you that it happened) and you will see the Access-Control-Allow-Origin in the response header. You will also see the response "Malformed request.".
I think the issue is that you're mixing the short form of the HTTP event (- http: GET /) with the long form that adds additional options.
Try using this:
functions:
app:
handler: handler.endpoint
events:
- http:
method: GET
path: /
cors:
origin: '*'
headers:
- Content-Type
- X-Amz-Date
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
- Startlower
- Text
- Access-Control-Allow-Headers
- Access-Control-Allow-Origin
allowCredentials: false
The main changes are:
1) Adding method and path keys on the http event object, and
2) Indenting the cors object another level. It was previously at the top level of the http event.
Let me know if this helps :)
The problem is how the Prefilght request working, in my experiencie this process doesn't work well only configuring the custom header on GET method, so, just add on POST method too and fix the CORS problem:
events:
- http:
path: admin/{type}
method: get
cors:
origin: '*'
headers:
- token
allowCredentials: false
- http:
path: admin/{type}
method: post
cors:
origin: '*'
headers:
- token
allowCredentials: false
This CORS and API Gateway survival guide and Fixing Common Problems with CORS and JavaScript help me to understand the problem and the solution.
Finally this is my JS code
fetch(api_url,{
method : 'GET',
mode: 'cors',
headers: {
token: 'XXXX-XXXX'
}
}).then((resp) => resp.json())

Cannot Validate Twilio Request in an AWS Lambda Custom Authorizer

I've built a custom authorizer lambda function with NodeJS, which I've configured to authorize another lambda function in AWS. This other function is triggered from an HTTP endpoint, and has the URL that I configured in my Twilio Messaging Service as the webhook URL with a GET method.
I have to use GET, because AWS does not include a POST request's headers into the input param of the authorizer function, and I need the header in order to get the X-Twilio-Signature.
In the authorizer function, I'm invoking the Twilio Node Helper validateRequest(token, signature, url, params) function and providing my auth token, the Twilio signature from the request header, and the exact same URL as configured in the webhook (no query params, no frills, just an https url with a path to the api resource).
However, the params is where I think things are breaking down and why the validation fails.
Since I'm using a GET method for the webhook, does that mean that when Twilio created the signature hash on their end, there was no POST data appended (per their docs on https://www.twilio.com/docs/api/security), or should I provide all the form data which they provide in the querysting of my GET request??
No matter what I've tried, my validation keeps failing as if the params I'm using are different than what Twilio did to create the signature.
I've created a simple test to see if I can validate the request using the params and signature of an actual HTTP request I made, but it never seems to work. Here's my simple test:
const token = '[my auth token]';
const url = 'https://my-api.company.io/sms/receive';
const signature = '[twilio header signature]';
const params = {
MessagingServiceSid: '[sid to my msg svc]',
ApiVersion: '2010-04-01',
SmsSid: 'SM6b3e14ea5e87ff967adb0c00c81406b8',
SmsStatus: 'received',
SmsMessageSid: 'SM6b3e14ea5e87ff967adb0c00c81406b8',
NumSegments: '1',
ToState: 'TX',
From: '+19998675309',
MessageSid: 'SM6b3e14ea5e87ff967adb0c00c81406b8',
AccountSid: '[my account sid]',
ToZip: '75229',
ToCity: 'DALLAS',
FromCountry: 'US',
FromCity: 'IRVING',
To: '[my twilio number]',
FromZip: '75014',
ToCountry: 'US',
Body: 'Super duper',
NumMedia: '0',
FromState: 'TX'
};
const result = twilio.validateRequest(token, signature, url, params);
console.log(result);
UPDATE
To respond to an answer from Phil (Twilio Dev Evangelist), here's what I see in the logs from my authorizer function when I switch to using a POST webhook URL (this wouldn't fit in a comment, so I'm editing the Q).
Note that this payload does not have any of the above mentioned parameters which are provided by Twilio in the body of the POST request and which I'd presumably need to provide to the twilio.validateRequest function:
{
type: 'REQUEST',
methodArn: 'arn:aws:execute-api:us-east-1:********:********/dev/POST/receive',
resource: '/receive',
path: '/sms/receive',
httpMethod: 'POST',
headers: {
Accept: '*/*',
'CloudFront-Viewer-Country': 'US',
'CloudFront-Forwarded-Proto': 'https',
'CloudFront-Is-Tablet-Viewer': 'false',
'CloudFront-Is-Mobile-Viewer': 'false',
'User-Agent': 'TwilioProxy/1.1',
'X-Forwarded-Proto': 'https',
'CloudFront-Is-SmartTV-Viewer': 'false',
Host: 'api.myredactedcompany.io',
'X-Forwarded-Port': '443',
'X-Amzn-Trace-Id': 'Root=**************',
Via: '1.1 ***************.cloudfront.net (CloudFront)',
'Cache-Control': 'max-age=259200',
'X-Twilio-Signature': '***************************',
'X-Amz-Cf-Id': '****************************',
'X-Forwarded-For': '[redacted IP addresses]',
'Content-Length': '492',
'CloudFront-Is-Desktop-Viewer': 'true',
'Content-Type': 'application/x-www-form-urlencoded'
},
queryStringParameters: {},
pathParameters: {},
stageVariables: {},
requestContext: {
path: '/sms/receive',
accountId: '************',
resourceId: '*****',
stage: 'dev',
requestId: '5458adda-ce2c-11e7-ba08-b7e69bc7c01c',
identity: {
cognitoIdentityPoolId: null,
accountId: null,
cognitoIdentityId: null,
caller: null,
apiKey: '',
sourceIp: '[redacted IP]',
accessKey: null,
cognitoAuthenticationType: null,
cognitoAuthenticationProvider: null,
userArn: null,
userAgent: 'TwilioProxy/1.1',
user: null
},
resourcePath: '/receive',
httpMethod: 'POST',
apiId: '*******'
}
}
Twilio developer evangelist here.
The issue here is that query string parameters are treated differently to POST body parameters when generating the signature.
Notably part 3 of the steps used to generate the request signature says:
If your request is a POST, Twilio takes all the POST fields, sorts them by alphabetically by their name, and concatenates the parameter name and value to the end of the URL (with no delimiter).
(Emphasis mine.)
This means that if you are trying to reconstruct the original URL, you will need to reconstruct the original query string with &s and =. The difficulty I can see here is that you don't know the original order of the parameters and I don't know if the order is not arbitrary.
Issue number 2 is that the Request authorizer will not send the POST body to a Lambda function.
So, either way you try to work it, a custom authorizer will never get all the details that the Twilio request validator requires to validate the request.
My only advice now is to move away from using the authorizers and just build request validation into your final Lambda function. It's not as nice as separating the concerns of validating the request and responding to the request, but since custom authorizers do not support all the features required, it's the only thing I can think of right now.
Now, the other thing that caught my eye was saying that you couldn't get the headers in a POST request authorizer. I had a look around and this Stack Overflow answer suggests it is now possible (only since September) to receive all the headers to a POST request to a custom authorizer, as long as you use the Request type authorizer and not a Token authorizer.
So my advice would be to change over to a Request authorizer, a POST request webhook from Twilio and the code you already have should work.
Let me know if that helps at all.

Resources