17
votes

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

7
did you have any luck with this?niqui

7 Answers

4
votes

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;
3
votes

I suspect it isn't possible, for two reasons.

IAM authentication -- specifically, Signature V4 -- has an implicit assumption that the hostname the client is accessing is also the hostname via which the service is being accessed.

The API Gateway endpoint expects the request to be signed with its own hostname as the host header used in the signing process. This could be worked around, by signing the request for the API Gateway endpoint, and then changing the URL to point to the CloudFront endpoint.

However, if you do that, I would expect that the x-amz-cf-id header that CloudFront adds to the request would also make passing through a valid signature impossible, because x-amz-* headers need to be signed -- which would be impossible, since you don't know that header's value.

I'm not sure there is a workaround, here... but if you are using IAM authentication, the only advantage of using CloudFront would be to keep the service under the same domain name as the rest of the site -- CloudFront wouldn't be able to cache any responses for authenticated requests, because each request's cache key would differ.

3
votes

It does support it, you just need to make HOST either that of your API GW or your API GW Custom domain that sits in front of it.

This is a difficult one to debug, I wrote a blog here going into more detail on the solution, hope it helps someone else. https://www.rehanvdm.com/serverless/cloudfront-reverse-proxy-api-gateway-to-prevent-cors/index.html

1
votes

API Gateway now generates Signature using the custom domain as host if a custom domain is setup for the API.

https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html

Manually creating a CloudFront distribution with API Gateway as origin does not work.

0
votes

Try go to your api gateway console and do the following:

  • Select your api
  • Go to Authorizers
  • Then click on Create New Authorizer Select Cognito and then select your userpool Set token source to Authorization
  • Click Create
  • Now go to resources and select which HTTP method you want to configure (e.g. ANY)
  • Click on Method request
  • On Authorization drop down select the one you create before and press the check.
  • Finally select the Actions and click Deploy API (select the stage that you want to deploy)

Then you need to get the jwtToken from the current user. The code below shows how it is done w/ ReactJS and amplify which configs CloudFront for you.

   Amplify.configure({
      Auth: {
            identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab',        
            region: 'XX-XXXX-X',         
            userPoolId: 'XX-XXXX-X_abcd1234',         
            userPoolWebClientId: 'a1b2c3d4e5f6g7h8i9j0k1l2m3',
      },
      API: {
        endpoints: [
          {
            name: 'myapi',
            endpoint: 'https://XXX',
             region: 'XX-XXXX-X',   
            custom_header: async () => ({ Authorization: (await Auth.currentSession()).idToken.jwtToken})
          }
        ]
});

But I think the steps to add Auth to the API is the same.

Hope that helps,

0
votes
  1. create a custom domain like www.example.com in APIGW and map that domain to the specific API, but DO NOT resolve www.example.com to APIGW's domain

  2. Resolve www.example.com to CloudFront's distribution domain. Set the Cache Based on Selected Request Headers to whitelist, add host, authorization and other headers necessary to the whitelist. The origin url is configured to APIGW's default url

  3. When the client use signature to access CF, the signature is generated with the domain www.example.com, then CF access APIGW with the same signature and the host is also www.example.com. When APIGW receives the signature, it calculate the signature with the domain it associates, which is still www.example.com. Then the signature is matched and APIGW respond correctly.

It worked for me

0
votes

Api gateway as origin in CF is usually fine until you try to run some API that is secured with gateway authorizer.

As Ray Liang said, it works if you set up custom domain in API Gateway settings. It is a nice feature and allows you do do a top level path mapping to place several different Gateways under a single domain.

The configuration of API gateway custom domain will generate a new proxy domain name (usually start with "d-"). You could CName or alias it to your real domain if you want users to access api gateway directly through that domain. In this case, you do not want to do that because you want users to access APi gateway through CloudFront. Therefore, Cloudfront distribution must be set up to be mapped to the real domain. And use this proxy domain (from APi gateway's custom domain setup) as an origin.

Then setup a behavior using that origin and make sure you let all headers through. This will pass the default Gateway authorizer because in API Gateway's eye the request is indeed signed using the proper domain name (API Gateway custom domain).