CloudFront背后的API网关是否不支持AWS_IAM身份验证?

时间:2018-02-15 19:47:38

标签: aws-sdk amazon-cloudfront aws-api-gateway amazon-cognito aws-amplify

似乎无法调用通过CloudFront分配启用AWS_IAM保护的REST API。

以下是重现此内容的方法:

  • 使用API​​网关创建REST API
  • 使用AWS_IAM身份验证保护REST API方法
  • 创建面向REST API的CloudFront分发
  • 在Route 53中创建一个以CloudFront Distribution
  • 为目标的A记录

现在使用经过身份验证的用户(我使用Cognito UserPool用户和aws-amplify)来调用

  1. 受保护的REST API方法及其API网关URL = SUCCESS
  2. 通过CloudFront分发网址= FAILURE
  3. 的受保护REST API方法
  4. 受保护的REST API方法,通过Route 53域URL = FAILURE
  5. 我得到的错误是:

    {" message":"我们计算的请求签名与您提供的签名不符。检查您的AWS Secret Access Key和签名方法。有关详细信息,请参阅服务文档。"}

    我无法相信AWS不支持自定义域后面的AWS_IAM保护端点,因为这必须是一个非常常见的用例。

    因此,您能否向我提供一份如何实现这一目标的详细清单?

    谢谢

7 个答案:

答案 0 :(得分:2)

我怀疑这是不可能的,原因有两个。

IAM身份验证 - 特别是Signature V4 - 隐含地假设客户端正在访问的主机名也是通过其访问服务的主机名。

API网关端点期望使用自己的主机名对请求进行签名,作为签名过程中使用的主机头。这可以通过签署API网关端点的请求,然后将URL更改为指向CloudFront端点来解决。

但是,如果您这样做,我希望CloudFront添加到请求的x-amz-cf-id标头也会使得无法通过有效签名,因为x-amz-*标头需要签名 - 这是不可能的,因为你不知道标题的值。

我不确定是否有解决方法,此处......但如果您使用的是IAM身份验证,则使用CloudFront的唯一优势是将服务保持在与网站其余部分相同的域名下 - CloudFront无法缓存经过身份验证的请求的任何响应,因为每个请求的缓存密钥都不同。

答案 1 :(得分:1)

如果为API设置了自定义域,API网关现在将使用该自定义域作为主机来生成签名。

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

以API网关作为原始资源手动创建CloudFront分配无效。

答案 2 :(得分:1)

它确实支持它,您只需将其主机的API GW或位于其前面的API GW自定义域设置为主机。

这是一个很难调试的人,我在这里写了一个博客,详细介绍了该解决方案,希望对其他人有帮助。 https://www.rehanvdm.com/serverless/cloudfront-reverse-proxy-api-gateway-to-prevent-cors/index.html

答案 3 :(得分:0)

CloudFront不支持对到达分配的呼叫进行IAM身份验证。正如其他人所强调的那样,SigV4依赖于主机标头,并且无法在访问您的域时计算签名(没有做一些棘手的事情,例如在客户端对API网关域进行硬编码,然后使用该标头对SigV4进行硬编码)。但是,您可以使用Lambda @ Edge函数将IAM从分发版本添加到API。

假设您已经将API网关设置为CloudFront分发的来源,则需要设置Lambda@Edge function来拦截来源请求,然后使用SigV4对其进行签名,以便可以限制API只能通过CloudFront访问的网关。

正常的HTTP请求和CloudFront event format之间有相当多的转换,但都可以管理。

首先,创建Lambda @ Edge函数(guide),然后确保其执行角色有权访问您要访问的API网关。为简单起见,您可以在Lambda的执行角色中使用AmazonAPIGatewayInvokeFullAccess托管的IAM策略,以使其有权调用帐户中的任何API网关。

然后,如果您使用aws4作为签名客户端,那么您的lambda代码将如下所示:

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;

答案 4 :(得分:0)

尝试转到您的api网关控制台并执行以下操作:

  • 选择您的api
  • 转到授权人
  • 然后单击“创建新授权者”,然后选择“ Cognito” 您的用户池将令牌源设置为“授权”
  • 点击创建
  • 现在转到资源,然后选择要使用的HTTP方法 配置(例如ANY)
  • 点击“方法请求”
  • 在“授权”下拉列表中,选择您之前创建的那个,然后按一下支票。
  • 最后选择“操作”,然后单击“部署API”(选择要部署的阶段)

然后,您需要从当前用户那里获取jwtToken。下面的代码显示了如何使用ReactJS进行操作,并为您放大了哪些CloudFront配置。

   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})
          }
        ]
});

但是我认为将Auth添加到API的步骤是相同的​​。

希望有帮助,

答案 5 :(得分:0)

  1. 在APIGW中创建一个自定义域,如www.example.com,并将该域映射到特定的API,但不要将www.example.com解析为APIGW的域

  2. 将www.example.com解析为CloudFront的分发域。将“基于选定的请求标头的缓存”设置为白名单,向白名单添加主机,授权和其他必要的标头。原始网址已配置为APIGW的默认网址

  3. 当客户端使用签名访问CF时,将使用域www.example.com生成签名,然后CF访问具有相同签名的APIGW,而主机也是www.example.com。当APIGW收到签名时,它将使用与其关联的域(仍为www.example.com)来计算签名。然后匹配签名,APIGW正确响应。

对我有用

答案 6 :(得分:0)

在 CF 中作为源的 Api 网关通常没问题,直到您尝试运行某些受网关授权方保护的 API。

正如 Ray Liang 所说,如果您在 API Gateway 设置中设置自定义域,它就会起作用。这是一个不错的功能,允许您进行顶级路径映射,以将多个不同的网关放置在单个域下。

API网关自定义域名的配置会生成一个新的代理域名(通常以“d-”开头)。如果您希望用户直接通过该域访问 api 网关,您可以将其 CName 或别名为您的真实域。在这种情况下,您不想这样做,因为您希望用户通过 CloudFront 访问 APi 网关。因此,必须将 Cloudfront 分布设置为映射到真实域。并使用此代理域(来自 APi 网关的自定义域设置)作为源。

然后使用该原点设置行为并确保让所有标题通过。这将通过默认网关授权方,因为在 API Gateway 看来,请求确实是使用正确的域名(API Gateway 自定义域)签名的。