How to allow CORS for custom headers in Serverless? - node.js

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())

Related

How can I get CORS to work for API calls with a Hapi v20 server with HTTPS?

Basic problem: you've followed the tutorial, you've fired up the Hapi server, it's running... but it doesn't work. A direct call via curl will get something, using a web browser to directly load the API call will get something... but using that API endpoint within an app, say, Angular or React, and it bombs out with an error message like:
Access to XMLHttpRequest at 'https://localhost:3000/server/ping' from origin 'http://localhost:5000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
And it's true: you check the headers, and Access-Control-Allow-Origin is not on that list at all. So your app, having gotten blocked here in the preflight request, isn't even going to make the actual GET/POST call.
Here's the full file of a fully working Hapi v20.2.0 server, in TypeScript:
'use strict'
import * as fs from 'fs'
import * as util from 'util'
import * as path from 'path'
import * as os from 'os'
import * as Hapi from '#hapi/hapi'
import * as Http2 from 'http2'
const strFullNameCert:string=path.resolve(
os.homedir(),
'ssl',
'domain.crt')
const strFullNameKey:string=path.resolve(
os.homedir(),
'ssl',
'domain.key')
const key :Buffer = fs.readFileSync(strFullNameKey)
const cert:Buffer = fs.readFileSync(strFullNameCert)
const sslDetails ={key,cert}
const server = new Hapi.server({
listener: Http2.createSecureServer(sslDetails), // optional: remove this line for http1.1
host: 'localhost',
port: 3000,
tls: sslDetails,
routes: {
cors: true
},
})
console.log(`Host : ${server.info.host}`)
console.log(`Port : ${server.info.port}`)
console.log(`Method : ${server.info.protocol}`)
console.log(`Hapi : v${server.version}`)
server.route({
method: 'GET',
path:'/server/ping',
handler: async (request, reply) => {
console.log(`>>>ROUTE>>> : ${request.route.path}`);
const response = reply.response({ qSuccess: true, qResult: 'pong' })
return response;
}
})
server.start()
To reiterate, this code will "work", it will serve up a response if you load the /server/ping route in an independent way. If you were building a web server to serve pages, this would likely be sufficient to get going.
This code will still fail CORS validation in a web app. Why? Because the request to /server/ping is never even going to be made. The app will send a preflight OPTIONS request first. And there's nothing in this code to handle that. So nothing you do in the server.route area, messing with route options, or adding headers, is going to fix this. Ten jillion different setups in the main server instantiation of routes:cors wont fix this, because they also don't address the actual problem.
I added these, at the top of my middleware, to respond to options(pre-flight) request.
Might cause problems in other areas of the app that use the options, but worked for my case/issue.
async function(request: Request, h: ResponseToolkit) {
if (request.method === "options") {
const response = h.response({});
response.header("Access-Control-Allow-Origin", "*");
response.header("Access-Control-Allow-Headers", "*");
return h.response(response).takeover();
}
// more checks....
},
The problem is there isn't a route set up that's dealing with the OPTIONS request that Chrome/Firefox will send before they attempt the GET/POST to the API being served up by Hapi. Add this code to the above file, just above server.start()
server.route({
method : 'OPTIONS',
path: '/{any*}',
handler : async (request, reply) => {
console.log(`======options: ${request.route.path}`)
const response = reply.response({})
response.header('Access-Control-Allow-Origin', '*')
response.header('Access-Control-Allow-Headers', '*')
return response;
}
})
Now you'll see that when you attempt to use the API from your app, it's going to make two calls: first, the options call, and then if that passes (and with these headers it now will) it will make the actual call you're interested in.
The "cors:true" route option, it really feels like it's going to "enable cors". I mean, yes... but really, no. Think of it more like it permits Hapi to do cors... you still have to do the work of "handling cors".

Missing 'accept-language' header in Express

I would like to use value of accept-language header for detecting language in node.js server created with using Express.
However when I try to get with headers:
console.dir(req.headers)
there is no accept-language field, other headers are present. But I can see 'Accept-Language' in chrome tab.
I am making request with JS fetch with this configuration:
fetch('/path/somepath', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(input)
})
What could be possible reasons of this behaviour and is something what I can do? I do not want to set this header explicitly in fetch because I would like it to create automatically on basis of browser settings.

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

Reactjs App request.get

I have a react js app which does a simple http get. It does not use webpack and use package.json
var request = require('request');
var options = {
url: 'http://localhost:8181/api/v1/status',
headers: {
'Access-Control-Allow-Origin':'*'
}
}
function callback(error, response, body) {
if (!error && response.statusCode == 200) {
var info = JSON.parse(body);
console.log(info.stargazers_count + " Stars");
console.log(info.forks_count + " Forks");
}
}
request(options, callback);
This code is getting failed and I am getting the following error
localhost/:1 Failed to load http://localhost:8181/api/v1/status: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. The response had HTTP status code 405. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
The server which I am trying to access is running python and I am already appending necessary header to make the request.
I am quite new to reactjs
How to fix this issue?
Let's breakdown the error message. The first part:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Means your Python server did not set an Access-Control-Allow-Origin header. You should make sure your python server sets this header in the response. If that didn't work, continue:
The response had HTTP status code 405.
HTTP Status Code 405 means method not allowed. Your python server may also need to set the header: Access-Control-Allow-Methods: <method>, <method>. So in include each one you want to allow, such as GET, POST, OPTIONS.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods

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