Cannot Validate Twilio Request in an AWS Lambda Custom Authorizer - node.js

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.

Related

How to get the req headers in lambda

I have try to send some data to AWS lambda and add data to database.I know how to get the header if using a local server but I don't know where and how to get the header using AWS. I have tried to find some possible solution but I still dont understand and solve my problem.
My api is something like that:
export const delectData = function (accessToken, id) {
return apiClient.post('end point get from API GATEWAY',
{ tableName: '***',
id: id },
{
headers: {
Authorization: `Bearer ${accessToken}`
}
})
I am using dynamodb to store my data. And I need to get the headers which is the accesstoken for doing validation. Can someone explain to me how to get the header or how to setup in APIGATE step by step? Since the doc in AWS is very unclear for me...
UPDATE: in my postman, It is :
Assuming your API is not a Lambda Proxy integration
1.Go to your method Integration Request
2.Click on Mapping Templates
3.Choose When no template matches the request Content-Type header
3.Click on application/json under Content-Type -> if does not exist create one.
4.Scroll down and put this template:
{
"YourHeader" : "$input.params('YourHeader')",
"body" : $input.json('$')
}
5.Redeploy your API.
6.In your Lambda event.YourHeader

cognitive computer vision api, Initial 202 response from api call, then 401 error from Operation-Location

Running this in a reactjs project, this is my current code:
let response = await fetch('https://apis-cv.cognitiveservices.azure.com/vision/v1.0/recognizeText?handwriting=true', {
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream', 'Ocp-Apim-Subscription-Key': '<MY KEY HERE>' },
body: this.makeblob(event.target.result)
});
console.log(response);
My initial response has status 202: with an Operation-Location given. (i.e https://apis-cv.cognitiveservices.azure.com/vision/v1.0/textOperations/a60b86b2-bf85-4e3b-8beb-65dc075e81d7 )
but the Operation-Location results in a 401:
{"error":{"code":"401","message":"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource."}}
I have also tried with a url and content-type: application/json and get the same result.
The error message is quite clear: you forgot to add the 'Ocp-Apim-Subscription-Key' in your second query, when you try to get the result of your TextOperations.
Can you add your implementation of how you try to get the result?

AWS lambda basic-authentication without custom authorizer

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'
}
}
}

Switching YouTube user with OAuth2 shows previous user's data in Chrome

I have a Chrome Packaged App that uses oauth2 to authenticate to YouTube. I'm using YouTube to determine the user's channel (via the channel endpoint).
It works for the first user authenticated. But if I switch to a different user, the same YouTube call returns the previous user's data (i.e. channel).
Here are the steps I'm going through.
I get my auth token via a call to getAuthToken:
chrome.identity.getAuthToken({ interactive: true, scopes: ['https://www.googleapis.com/auth/youtube'] })
I get their channel information. I make a call to the channels endpoint like so:
const url = 'https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true';
const headers = { Authorization: `Bearer ${token}` };
const config = {
url,
method: 'get',
headers,
responseType: 'json',
};
return axios(config);
This works! The result of the call gives me the channel information.
The user removes their account. I use the same sequence of calls that the Google Demo uses (like this example):
a. call chrome.identiy.removeCachedAuthToken({ token }) with the token
b. call https://accounts.google.com/o/oauth2/revoke?token=${token} to revoke it
Everything is cleared out, I think.
If I look at chrome://identity-internals/ I still see the token, but the Token Status is set to Not Found
The issue:
I repeat from step 1, but I chose a different user.
I confirm that I get a new token that is different than the one I had previously
The call to the YouTube channels api returns the previous user's channel.
It turns out it was a caching issue with youtube.
I had to add 'Cache-Control': 'no-cache' to my headers.
Here is the full headers line:
const headers = {
Authorization: `Bearer ${token}`,
'Cache-Control': 'no-cache',
};

Sending URL encoded string in POST request using node.js

I am having difficulty sending a url encoded string to the Stormpath /oauth/token API endpoint. The string is meant to look like this:
grant_type=password&username=<username>&password=<password>
Using Postman I was successful in hitting the endpoint and retrieving the data I want by providing a string similar to the one above in the request body by selecting the raw / text option. But when I generate the code snippet it looks like this:
var request = require("request");
var options = { method: 'POST',
url: 'https://<My DNS label>.apps.stormpath.io/oauth/token',
headers:
{ 'postman-token': '<token>',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded',
host: '<My DNS label>.apps.stormpath.io',
accept: 'application/json' },
form: false };
request(options, function (error, response, body) {
if (error) throw new Error(error);
console.log(body);
});
Where did that string go? I would like some help in understanding the disconnect between knowing I sent a url encoded string to the API endpoint using Postman and not seeing it in the code generated by Postman. Because now I don't know how to reproduce a successful call to the endpoint in my actual app.
To me it seems like I should simply provide a body to the request, but the response comes out to be {"error":"invalid_request","message":"invalid_request"}. I have also tried appending the url encoded string to the url but that returns a 404 error.
I'm just now getting back into using an API and am not very experienced doing so.
The form data needs to be posted as an object, here is an example:
request.post('http://service.com/upload', {form:{key:'value'}})
Taken from this documentation:
https://github.com/request/request#forms
Hope this helps!

Resources