How do you pass Authorization header through API Gateway to HTTP endpoint? - amazon-web-services

I have an API behind an AWS API Gateway that needs to use the Authorization header for processing. I have unfortunately been unable to pass this to the backend for processing.
I have tried creating the Authorization HTTP Request Header in my Method Request and then creating the corresponding Authorization HTTP Header in my Integration Request (Authorization is mapped from method.request.header.Authorization in this case). I log all of the headers that the backend receives, and from the log, I can see other headers that I have listed in the Integration Request but not Authorization.
I have also tried creating a mapping template with Content-Type application/json and the template defined as
{
"AccountID": "$context.identity.accountId",
"Caller": "$context.identity.caller",
"User": "$context.identity.user",
"Authorization": "$input.params().header.get('Authorization')",
"UserARN": "$context.identity.userArn"
}
Yet, the backend logs show that there is still no Authorization header nor any Authorization field in the JSON body. I also cannot see the user's ARN. I have seen other examples and threads where users have mentioned accessing the Authorization field on the event object that is passed into a Lambda function, but I am not using a Lambda function.
I have made sure to Deploy the API Gateway in both scenarios.
Does anyone know if there is some way I can pass the Authorization header through the API Gateway to my HTTP endpoint? Is there an alternative way to access the API caller's user name or ID?
Edit - Here's a snippet of the code I'm using to hit the API Gateway:
String awsAccessKey = "myaccesskey";
String awsSecretKey = "mysecretkey";
URL endpointUrl;
try {
endpointUrl = new URL("https://<host>/<path>/<to>/<resource>?startDate=20151201&endDate=20151231");
} catch(Exception e) {
throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
}
Date now = new Date();
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
sdf1.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateTS = sdf1.format(now);
String headerNames = "host;x-amz-date";
String queryParameters = "endDate=20151231&startDate=20151201";
String canonicalRequest = "GET\n" +
"/<path>/<to>/<resource>\n" +
queryParameters + "\n" +
"host:<host>\n" +
"x-amz-date:" + dateTS + "\n" +
"\n" +
headerNames + "\n" +
"<sha256 hash for empty request body>";
System.out.println(canonicalRequest);
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd");
sdf2.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateStr = sdf2.format(now);
String scope = dateStr + "/us-east-1/execute-api/aws4_request";
String stringToSign =
"AWS4-HMAC-SHA256\n" +
dateTS + "\n" +
scope + "\n" +
"hex encoded hash of canonicalRequest";
System.out.println(stringToSign);
byte[] kSecret = ("AWS4" + awsSecretKey).getBytes();
byte[] kDate = HmacSHA256(dateStr, kSecret);
byte[] kRegion = HmacSHA256("us-east-1", kDate);
byte[] kService = HmacSHA256("execute-api", kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
byte[] signature = HmacSHA256(stringToSign, kSigning);
String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
String signedHeadersAuthorizationHeader = "SignedHeaders=" + headerNames;
String signatureAuthorizationHeader = "Signature=" + "hex encoded signature";
String authorization = "AWS4-HMAC-SHA256 "
+ credentialsAuthorizationHeader + ", "
+ signedHeadersAuthorizationHeader + ", "
+ signatureAuthorizationHeader;
Map<String, String> headers = new HashMap<String, String>();
headers.put("x-amz-date", dateTS);
headers.put("Host", endpointUrl.getHost());
headers.put("Authorization", authorization);
headers.put("Content-Type", "application/json");
HttpURLConnection connection = null;
try {
connection = (HttpURLConnection) endpointUrl.openConnection();
connection.setRequestMethod("GET");
for (String headerKey : headers.keySet()) {
connection.setRequestProperty(headerKey, headers.get(headerKey));
}
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream is;
try {
is = connection.getInputStream();
} catch (IOException e) {
is = connection.getErrorStream();
}
BufferedReader rd = new BufferedReader(new InputStreamReader(is));
String line;
StringBuffer response = new StringBuffer();
while ((line = rd.readLine()) != null) {
response.append(line);
response.append('\r');
}
rd.close();
System.out.println(response.toString());
} catch (Exception e) {
throw new RuntimeException("Error: " + e.getMessage(), e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
This is good enough to authenticate successfully and hit the HTTP endpoint on the backend.

As noted in comments, the Authorization header includes incomplete information for you to establish who the user is, so I wouldn't recommend going this route. Additionally, if AWS_IAM auth is enabled, the Authorization header will be consumed by API Gateway.
If AWS_IAM auth is enabled and the signature is supplied correctly, the $context.identity parameters should reflect the credentials used to sign the request.
If you use the test invoke feature in the console, do you see the context fields being filled in?
Update:
I'm unable to reproduce this issue.
I have an API with the following mapping template:
#set($path = $input.params().path)
#set($qs = $input.params().querystring)
{
"resource-path": "$context.resourcePath",
"http-method": "$context.httpMethod",
"identity": {
#foreach($key in $context.identity.keySet())
"$key": "$context.identity.get($key)"
#if($foreach.hasNext), #end
#end
},
"params": {
#foreach($key in $path.keySet())
"$key": "$path.get($key)"
#if($foreach.hasNext), #end
#end
},
"query": {
#foreach($key in $qs.keySet())
"$key": "$qs.get($key)"
#if($foreach.hasNext), #end
#end
},
"body": $input.json('$')
}
And a lambda function that simply spits back the input as output. When I sign the request and invoke the API, I get back the expected results:
{
"resource-path":"/iam",
"http-method":"GET",
"identity":{
"cognitoIdentityPoolId":"",
"accountId":"xxxxxxxx",
"cognitoIdentityId":"",
"caller":"AIDXXXXXXXXXXX,
"apiKey":"",
"sourceIp":"54.xx.xx.xx",
"cognitoAuthenticationType":"",
"cognitoAuthenticationProvider":"",
"userArn":"arn:aws:iam::xxxxxxxx:user/hackathon",
"userAgent":"Java/1.8.0_31",
"user":"AIDXXXXXXXXXXXXXX"
},
"params":{},
"query":{},
"body":{}
}

Currently the Authorization header can only be forwarded for methods that do not require AWS authentication. The SigV4 signing process relies on the Authorization header and we do not expose this for security purposes. If you have data you need to send (besides the SigV4 signature), you would need to send in another header.

In AWS API Gateway, Request Body is not supported for GET methods.

In Integration Request convert your GET to POST by specifying POST as your HTTP method. Then proceed with specifying the Body Mapping Template as proposed by #BobKinney
This way the request body will propagate properly, but the client will still be making a GET request as expected

Related

Cannot read properties of undefined (reading 'startsWith') error while deploying lambda edge function

In AWS I am configuring CloudFront with S3 Origin. The S3 is configured for Server-Side Encryption with Customer Master Keys (CMKs). Because of the coudfront issue with S3 with CMK encryption, we have to use Lambda#Edge function as suggested in this AWS Article
The Node JS code below is copied from the article linked
// Declare constants reqiured for the signature process
const crypto = require('crypto');
const emptyHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const signedHeadersGeneric = 'host;x-amz-content-sha256;x-amz-date;x-amz-security-token';
// CloudFront includes the x-amz-cf-id header in the signature for custom origins
const signedHeadersCustomOrigin = 'host;x-amz-cf-id;x-amz-content-sha256;x-amz-date;x-amz-security-token';
// Retrieve the temporary IAM credentials of the function that were granted by
// the Lambda#Edge service based on the function permissions. In this solution, the function
// is given permissions to read from S3 and decrypt using the KMS key.
const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN } = process.env;
// Since the function is configured to be executed on origin request events, the handler
// is executed every time CloudFront needs to go back to the origin, which is S3 here.
exports.handler = async event => {
// Retrieve the original request that CloudFront was going to send to S3
const request = event.Records[0].cf.request;
// The request object has different properties depending on the type of
// origin that is being used. Account for that here.
let originType = '';
if (request.origin.hasOwnProperty('s3'))
originType = 's3';
else if (request.origin.hasOwnProperty('custom'))
originType = 'custom';
else
throw("Unexpected origin type. Expected 's3' or 'custom'. Got: " + JSON.stringify(request.origin));
// Create a JSON object with the fields that should be included in the Sigv4 request,
// including the X-Amz-Cf-Id header that CloudFront adds to every request forwarded
// upstream. This header is exposed to Lambda#Edge in the event object
const sigv4Options = {
method: request.method,
path: request.origin[originType].path + request.uri,
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN
},
host: request.headers['host'][0].value,
xAmzCfId: event.Records[0].cf.config.requestId,
originType: originType
};
// Compute the signature object that includes the following headers: X-Amz-Security-Token, Authorization,
// X-Amz-Date, X-Amz-Content-Sha256, and X-Amz-Security-Token
const signature = signV4(sigv4Options);
// Finally, add the signature headers to the request before it is sent to S3
for(var header in signature){
request.headers[header.toLowerCase()] = [{
key: header,
value: signature[header].toString()
}];
}
return request;
};
// Helper functions to sign the request using AWS Signature Version 4
// This helper only works for S3, using GET/HEAD requests, without query strings
// https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
function signV4(options) {
// Infer the region from the host header
const region = options.host.split('.')[2];
// Create the canonical request
const date = (new Date()).toISOString().replace(/[:-]|\.\d{3}/g, '');
let canonicalHeaders = '';
let signedHeaders = '';
if (options.originType == 's3') {
canonicalHeaders = ['host:'+options.host, 'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n');
signedHeaders = signedHeadersGeneric;
} else {
canonicalHeaders = ['host:'+options.host, 'x-amz-cf-id:'+options.xAmzCfId, 'x-amz-content-sha256:'+emptyHash, 'x-amz-date:'+date, 'x-amz-security-token:'+options.credentials.sessionToken].join('\n');
signedHeaders = signedHeadersCustomOrigin;
}
const canonicalURI = encodeRfc3986(encodeURIComponent(decodeURIComponent(options.path).replace(/\+/g, ' ')).replace(/%2F/g, '/'));
const canonicalRequest = [options.method, canonicalURI, '', canonicalHeaders + '\n', signedHeaders,emptyHash].join('\n');
// Create string to sign
const credentialScope = [date.slice(0, 8), region, 's3/aws4_request'].join('/');
const stringToSign = ['AWS4-HMAC-SHA256', date, credentialScope, hash(canonicalRequest, 'hex')].join('\n');
// Calculate the signature
const signature = hmac(hmac(hmac(hmac(hmac('AWS4' + options.credentials.secretAccessKey, date.slice(0, 8)), region), "s3"), 'aws4_request'), stringToSign, 'hex');
// Form the authorization header
const authorizationHeader = ['AWS4-HMAC-SHA256 Credential=' + options.credentials.accessKeyId + '/' + credentialScope,'SignedHeaders=' + signedHeaders,'Signature=' + signature].join(', ');
// return required headers for Sigv4 to be added to the request to S3
return {
'Authorization': authorizationHeader,
'X-Amz-Content-Sha256' : emptyHash,
'X-Amz-Date': date,
'X-Amz-Security-Token': options.credentials.sessionToken
};
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
}
function hash(string, encoding) {
return crypto.createHash('sha256').update(string, 'utf8').digest(encoding)
}
function hmac(key, string, encoding) {
return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding)
}
However when I try to deploy the lambda#edge function with the suggested Node JS code it throws error Cannot read properties of undefined (reading 'startsWith')
What could be the issue here
Select in dropdown "viewer response" or "origin response" and after that return to "origin request".

How to google oauth to an api? My example is not working

I am trying to do this article for google cloud build
https://cloud.google.com/endpoints/docs/openapi/service-account-authentication
I am guessing to use the service account email I generated the key from in that example AND for Audient, I put "" (which is probably the reason it's not working?). I have no idea and can't find what in the world to put for audience.
In addition to code below, I tried setting audience to 'https://cloudbuild.googleapis.com' which also did not work
My code is the following...
public class GenToken {
public static void main(String[] args) throws IOException {
Duration d = Duration.ofDays(365);
String tok = generateJwt("/Users/dean/workspace/order/java/googleBuild/orderly-gcp-key.json",
"mycloudbuilder#order-gcp.iam.gserviceaccount.com", "", d.toSeconds());
System.out.println("tok="+tok);
URL url = new URL("https://cloudbuild.googleapis.com/v1/projects/order-gcp/builds");
makeJwtRequest(tok, "GET", url);
}
public static String generateJwt(final String saKeyfile, final String saEmail,
final String audience, final long expiryLength)
throws FileNotFoundException, IOException {
Date now = new Date();
Date expTime = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiryLength));
// Build the JWT payload
JWTCreator.Builder token = JWT.create()
.withIssuedAt(now)
// Expires after 'expiraryLength' seconds
.withExpiresAt(expTime)
// Must match 'issuer' in the security configuration in your
// swagger spec (e.g. service account email)
.withIssuer(saEmail)
// Must be either your Endpoints service name, or match the value
// specified as the 'x-google-audience' in the OpenAPI document
.withAudience(audience)
// Subject and email should match the service account's email
.withSubject(saEmail)
.withClaim("email", saEmail);
// Sign the JWT with a service account
FileInputStream stream = new FileInputStream(saKeyfile);
ServiceAccountCredentials cred = ServiceAccountCredentials.fromStream(stream);
RSAPrivateKey key = (RSAPrivateKey) cred.getPrivateKey();
Algorithm algorithm = Algorithm.RSA256(null, key);
return token.sign(algorithm);
}
/**
* Makes an authorized request to the endpoint.
*/
public static String makeJwtRequest(final String signedJwt, String method, final URL url)
throws IOException, ProtocolException {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod(method);
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Authorization", "Bearer " + signedJwt);
InputStreamReader reader = new InputStreamReader(con.getInputStream());
BufferedReader buffReader = new BufferedReader(reader);
String line;
StringBuilder result = new StringBuilder();
while ((line = buffReader.readLine()) != null) {
result.append(line);
}
buffReader.close();
return result.toString();
}
}
The orderly-gcp-key.json has these attributes in it
{
"type": "service_account",
"project_id": "myproj",
"private_key_id": "xxxxxxxx",
"private_key": "-----BEGIN PRIVATE KEY-----\nasdfsd\n-----END PRIVATE KEY-----\n",
"client_email": "build-ci-mine#myproj.iam.gserviceaccount.com",
"client_id": "1167333552",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/build-ci-mine%40myproj.iam.gserviceaccount.com"
}
oops, my edit didn't get posted :(. Here is the error
Exception in thread "main" java.io.IOException: Server returned HTTP response code: 401 for URL: https://cloudbuild.googleapis.com/v1/projects/orderly-gcp/builds
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1919)
at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1515)
at java.base/sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:250)
at com.orderlyhealth.auth.websecure.GenToken.makeJwtRequest(GenToken.java:71)
at com.orderlyhealth.auth.websecure.GenToken.main(GenToken.java:26)
I hope that I better understood!!
When you try to reach a Google API, you have to use an access token. I have 2 code snippets for you.
Use Google Http client
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
HttpRequestFactory factory = new NetHttpTransport().createRequestFactory(new HttpCredentialsAdapter(credentials));
HttpRequest request = factory.buildGetRequest(new GenericUrl("https://cloudbuild.googleapis.com/v1/projects/gbl-imt-homerider-basguillaueb/builds"));
HttpResponse httpResponse = request.execute();
System.out.println(CharStreams.toString(new InputStreamReader(httpResponse.getContent(), Charsets.UTF_8)));
Use pure java connection
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
HttpURLConnection con = (HttpURLConnection) new URL("https://cloudbuild.googleapis.com/v1/projects/gbl-imt-homerider-basguillaueb/builds").openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Content-Type", "application/json");
con.setRequestProperty("Authorization", "Bearer " + credentials.refreshAccessToken().getTokenValue());
InputStreamReader reader = new InputStreamReader(con.getInputStream());
BufferedReader buffReader = new BufferedReader(reader);
String line;
StringBuilder result = new StringBuilder();
while ((line = buffReader.readLine()) != null) {
result.append(line);
}
buffReader.close();
System.out.println(result.toString());
You can rely on the platform environment. In local, perform a gcloud auth application-default login to set your credential as default default credential. On GCP, the component identity (the default service account or the service account that you define when you create the component), is used thanks to the method GoogleCredentials.getApplicationDefault();
Your dependency management need this (here in maven)
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>0.20.0</version>
</dependency>
Does this solve your issue?

API Gateway -> Lambda -> DynamoDB using Cognito. HTTP POST-> Unable to read response but returns a code 200

Scenario:
I query an HTTP POST (using Authorizer as Header parameter from Cognito).
When I try to fetch/read the query response, it triggers the error event. However, in the browser, I can see how 2 HTTP POST responses with 200 code and one of them returning the valid response. For example: if I make the request via POST man I receive the data in 1 response in a good way.
Problem:
I am unable to print the result because it launches the error event with not valid response data.
Browser images:
https://i.postimg.cc/MTMsxZjw/Screenshot-1.png
https://i.postimg.cc/3RstwMgv/Screenshot-2.png
Lambda code:
'use strict';
var AWS = require('aws-sdk'),
documentClient = new AWS.DynamoDB.DocumentClient();
exports.handler = function index(event, context, callback){
var params = {
TableName : "data-table"
};
documentClient.scan(params, function(err, data){
if(err){
callback(err, null);
}else{
console.log(JSON.stringify(data.Items));
callback(null, data.Items);
}
});
}
Client side JS code:
function requestData(pickupLocation) {
$.ajax({
type: 'POST',
url: _config.api.invokeUrl,
headers: {
Authorization: authToken,
},
data: "{}",
cache: false,
success: completeRequest,
error: errorRequest
});
}
function completeRequest(response) {
alert("hello");
alert(response.d);
}
function errorRequest(response) {
alert("hello1");
alert(response.status + ' ' + response.statusText);
}
According to further clarification based on the comments, this looks like API gateway has CORS disabled or enabled with incorrect header value returns.
The solution is to re-enable CORS through API gateway and in the advanced options add Access-Control-Allow-Origin to the header response (if not already on by default).
If you're proxying the response, you need to follow a specific format as described here
'use strict';
console.log('Loading hello world function');
exports.handler = async (event) => {
let name = "you";
let city = 'World';
let time = 'day';
let day = '';
let responseCode = 200;
console.log("request: " + JSON.stringify(event));
// This is a simple illustration of app-specific logic to return the response.
// Although only 'event.queryStringParameters' are used here, other request data,
// such as 'event.headers', 'event.pathParameters', 'event.body', 'event.stageVariables',
// and 'event.requestContext' can be used to determine what response to return.
//
if (event.queryStringParameters && event.queryStringParameters.name) {
console.log("Received name: " + event.queryStringParameters.name);
name = event.queryStringParameters.name;
}
if (event.pathParameters && event.pathParameters.proxy) {
console.log("Received proxy: " + event.pathParameters.proxy);
city = event.pathParameters.proxy;
}
if (event.headers && event.headers['day']) {
console.log("Received day: " + event.headers.day);
day = event.headers.day;
}
if (event.body) {
let body = JSON.parse(event.body)
if (body.time)
time = body.time;
}
let greeting = `Good ${time}, ${name} of ${city}. `;
if (day) greeting += `Happy ${day}!`;
let responseBody = {
message: greeting,
input: event
};
// The output from a Lambda proxy integration must be
// of the following JSON object. The 'headers' property
// is for custom response headers in addition to standard
// ones. The 'body' property must be a JSON string. For
// base64-encoded payload, you must also set the 'isBase64Encoded'
// property to 'true'.
let response = {
statusCode: responseCode,
headers: {
"x-custom-header" : "my custom header value"
},
body: JSON.stringify(responseBody)
};
console.log("response: " + JSON.stringify(response))
return response;
};
If you are using chrome you probably need the cors plugin .

How to consume a webapi in webservice using .net framework 2.0

I need to consume a webapi service which is developed using java. While connecting to the api I am getting error as "Unable to retrieve resources for ' '".
When I consume the same api through soapUI I am able to get response.
Am I missing something?
using (WebClient client = new WebClient())
{
try
{
client.Headers.Clear();
client.Headers[HttpRequestHeader.ContentType] = "application/xml";
client.Headers[HttpRequestHeader.Accept] = "application/xml";
byte[] data = Encoding.UTF8.GetBytes(StrRequestXML);
byte[] result = client.UploadData("url+soapaction", "POST", data);
string xmlResponse = System.Text.Encoding.UTF8.GetString(result);
}
catch (WebException ex)
{
throw ex;
}
}
I think the problem is here:
byte[] result = client.UploadData("url+soapaction", "POST", data);
you pass string "url+soapaction" but there should be The URI of the resource to receive the data.
Maybe you mean
byte[] result = client.UploadData(url + soapaction, "POST", data);

Using AWS Gateway API, can I access the cookies?

Using a HTTP Proxy Integration I want to access the cookies and add one to the json response. Is that possible?
To access cookies sent by the client in your backend you'll have to setup a mapping from the method request header to your integration request header.
These instructions assume you've already setup a simple method in API Gateway.
Access cookies in your backend
Under Method Request, create an HTTP Request Header with the name of "Cookie"
Under Integration Request, create an HTTP header with name "Cookie" and "Mapped from" value of method.request.header.Cookie.
You'll also likely need to setup CORS for this method. See: http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html
Deploy your API and make a request to your API Gateway endpoint with your browser/client. You should see requests coming in to your HTTP backend with the Cookie header value sent from the browser.
Add cookie to response
You can setup a Set-Cookie response header in an analogous fashion for the the integration response/method response side of the method configuration.
Under Method Response, create a Response header with name Set-Cookie
Under Integration Response setup a Header Mapping with Response header Set-Cookie and Mapping value integration.response.header.Set-Cookie
Please note that at this time, API Gateway supports setting just a single Set-Cookie response header. If your backend attempts to set multiple Set-Cookie headers, only the last one will be set. See this forum post for more details: https://forums.aws.amazon.com/thread.jspa?messageID=701434
If you check the "Use Lambda Proxy integration" option in your API Gateway method, the request headers will be passed to your Lambda function via the event variable. API Gateway will also expect a different response from your callback function. This response format can be use to dictate a Set-Cookie header. e.g.:
callback(null, {
statusCode: 200,
headers: {'Set-Cookie': 'key=val'},
body: 'Some response'
})`
This approach has the advantage of not requiring any Method Request or Method Response tweaks.
Here's a sample Lambda function using this logic to rotate a cookie value after each request.
exports.handler = (event, context, callback) => {
var cookies = getCookiesFromHeader(event.headers);
var old_cookie = cookies.flavor;
var new_cookie = pickCookieFlavor(old_cookie);
return callback(null, {
statusCode: 200,
headers: {
'Set-Cookie': setCookieString('flavor', new_cookie),
'Content-Type': 'text/plain'
},
body: 'Your cookie flavor was ' + old_cookie + '. Your new flavor is ' + new_cookie + '.'
});
};
/**
* Rotate the cookie flavor
*/
function pickCookieFlavor(cookie) {
switch (cookie) {
case 'peanut':
return 'chocolate';
case 'chocolate':
return 'raisin and oat';
default:
return 'peanut';
}
}
/**
* Receives an array of headers and extract the value from the cookie header
* #param {String} errors List of errors
* #return {Object}
*/
function getCookiesFromHeader(headers) {
if (headers === null || headers === undefined || headers.Cookie === undefined) {
return {};
}
// Split a cookie string in an array (Originally found http://stackoverflow.com/a/3409200/1427439)
var list = {},
rc = headers.Cookie;
rc && rc.split(';').forEach(function( cookie ) {
var parts = cookie.split('=');
var key = parts.shift().trim()
var value = decodeURI(parts.join('='));
if (key != '') {
list[key] = value
}
});
return list;
};
/**
* Build a string appropriate for a `Set-Cookie` header.
* #param {string} key Key-name for the cookie.
* #param {string} value Value to assign to the cookie.
* #param {object} options Optional parameter that can be use to define additional option for the cookie.
* ```
* {
* secure: boolean // Watever to output the secure flag. Defaults to true.
* httpOnly: boolean // Watever to ouput the HttpOnly flag. Defaults to true.
* domain: string // Domain to which the limit the cookie. Default to not being outputted.
* path: string // Path to which to limit the cookie. Defaults to '/'
* expires: UTC string or Date // When this cookie should expire. Default to not being outputted.
* maxAge: integer // Max age of the cookie in seconds. For compatibility with IE, this will be converted to a
* `expires` flag. If both the expires and maxAge flags are set, maxAge will be ignores. Default to not being
* outputted.
* }
* ```
* #return string
*/
function setCookieString(key, value, options) {
var defaults = {
secure: true,
httpOnly: true,
domain: false,
path: '/',
expires: false,
maxAge: false
}
if (typeof options == 'object') {
options = Object.assign({}, defaults, options);
} else {
options = defaults;
}
var cookie = key + '=' + value;
if (options.domain) {
cookie = cookie + '; domain=' + options.domain;
}
if (options.path) {
cookie = cookie + '; path=' + options.path;
}
if (!options.expires && options.maxAge) {
options.expires = new Date(new Date().getTime() + parseInt(options.maxAge) * 1000); // JS operate in Milli-seconds
}
if (typeof options.expires == "object" && typeof options.expires.toUTCString) {
options.expires = options.expires.toUTCString();
}
if (options.expires) {
cookie = cookie + '; expires=' + options.expires.toString();
}
if (options.secure) {
cookie = cookie + '; Secure';
}
if (options.httpOnly) {
cookie = cookie + '; HttpOnly';
}
return cookie;
}