Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does API Gateway behind CloudFront not support AWS_IAM authentication?

It seems that it is impossible to call a REST API that has AWS_IAM protection enabled through a CloudFront Distribution.

Here is how to reproduce this:

  • create a REST API with API Gateway
  • protect a REST API method with AWS_IAM authentication
  • create a CloudFront Distribution that targets the REST API
  • create an A Record in Route 53 that targets the CloudFront Distribution

Now use an authenticated user (I use Cognito UserPool user and aws-amplify) to call

  1. the protected REST API method with its API Gateway URL = SUCCESS
  2. the protected REST API method via the CloudFront distribution URL = FAILURE
  3. the protected REST API method via the Route 53 domain URL = FAILURE

The error I am getting is:

{"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."}

I just can't believe AWS does not support AWS_IAM protected endpoints behind a custom domain since this must be a very very common use-case.

Therefore could you please provide me with a detailed list of how to achieve this?

Thank you

like image 248
Christine Avatar asked Feb 15 '18 19:02

Christine


1 Answers

CloudFront does not support IAM auth for calls hitting the distribution. As others have highlighted, SigV4 relies on the host header and there is no way to calculate a signature while hitting your domain (without doing something hacky like hardcoding the API Gateway domain on the client side and then SigV4 with that header). You can, however, add IAM from your distribution to your API using a Lambda@Edge function.

Assuming that you have already setup API Gateway as an origin for your CloudFront distribution, you need to setup a Lambda@Edge function that intercepts origin requests and then signs it using SigV4 so that you can restrict your API Gateway to access only via CloudFront.

There is a fair amount of conversion between normal HTTP requests and the CloudFront event format but it is all manageable.

First, create a Lambda@Edge function (guide) and then ensure its execution role has access to the API Gateway that you would like to access. For simplicity, you can use the AmazonAPIGatewayInvokeFullAccess managed IAM policy in your Lambda's execution role which gives it access to invoke any API Gateway within your account.

Then, if you go with using aws4 as your signing client, this is what your lambda code would look like:

const aws4 = require("aws4");

const signCloudFrontOriginRequest = (request) => {
  const searchString = request.querystring === "" ? "" : `?${request.querystring}`;

  // Utilize a dummy request because the structure of the CloudFront origin request
  // is different than the signing client expects
  const dummyRequest = {
    host: request.origin.custom.domainName,
    method: request.method,
    path: `${request.origin.custom.path}${request.uri}${searchString}`,
  };

  if (Object.hasOwnProperty.call(request, 'body')) {
    const { data, encoding } = request.body;
    const buffer = Buffer.from(data, encoding);
    const decodedBody = buffer.toString('utf8');

    if (decodedBody !== '') {
      dummyRequest.body = decodedBody;
      dummyRequest.headers = { 'content-type': request.headers['content-type'][0].value };
    }
  }

  // Use the Lambda's execution role credentials
  const credentials = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN
  };

  aws4.sign(dummyRequest, credentials); // Signs the dummyRequest object

  // Sign a clone of the CloudFront origin request with appropriate headers from the signed dummyRequest
  const signedRequest = JSON.parse(JSON.stringify(request));
  signedRequest.headers.authorization = [ { key: "Authorization", value: dummyRequest.headers.Authorization } ];
  signedRequest.headers["x-amz-date"] = [ { key: "X-Amz-Date", value: dummyRequest.headers["X-Amz-Date"] } ];
  signedRequest.headers["x-amz-security-token"] = [ { key: "X-Amz-Security-Token", value: dummyRequest.headers["X-Amz-Security-Token"] } ];

  return signedRequest;
};

const handler = (event, context, callback) => {
  const request = event.Records[0].cf.request;
  const signedRequest = signCloudFrontOriginRequest(request);

  callback(null, signedRequest);
};

module.exports.handler = handler;
like image 151
Reed Hermes Avatar answered Sep 22 '22 18:09

Reed Hermes