Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apigateway: add explicit support for CORS #906

Closed
13 of 16 tasks
eladb opened this issue Oct 11, 2018 · 33 comments · Fixed by #4211
Closed
13 of 16 tasks

apigateway: add explicit support for CORS #906

eladb opened this issue Oct 11, 2018 · 33 comments · Fixed by #4211
Assignees
Labels
@aws-cdk/aws-apigateway Related to Amazon API Gateway feature-request A feature should be added or improved. management/tracking Issues that track a subject or multiple issues

Comments

@eladb
Copy link
Contributor

eladb commented Oct 11, 2018

Requirements

  • resource.addCorsPreflight(options)
  • AllowOrigin
  • AllowHeaders with defaults for API Gateway
  • AllowMethods
  • AllowCredentials
  • MaxAge
  • ExposeHeaders
  • Conditional Vary response header
  • Configure response status code (should default to 204)
  • Support proxy and non-proxy resources
  • Support multiple origins through velocity templates (like in serverless framework).
  • Automatically discover allowed methods by default based on model.
  • Recursive (apply to all child resources)
  • LambdaRestApi (apply a CORS policy to all routes)
  • CORS with custom authorizers (good post) through AWS::ApiGateway::GatewayResponse

Nice to Have

  • Dynamic CORS handler? (through a Lambda proxy and Access-Control-Max-Age=0)

Non-Requirements

Resources

Notes

Coming from stack overflow

Note that we get a lot of confusion around this since it only configures the Preflight request. Customers expect it to be a magic setting for enabling CORS headers on their Lambda responses. We cant do this because API Gateway does not allow response header mapping for Lambda proxy. Might be better to call this CorsPreflight? At minimum document it clearly.

@eladb eladb added the feature label Oct 14, 2018
@eladb
Copy link
Contributor Author

eladb commented Oct 24, 2018

From @kennu:

function addCorsOptions(apiResource: apigateway.IRestApiResource) {
  const options = apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
    integrationResponses: [{
      statusCode: '200',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
        'method.response.header.Access-Control-Allow-Origin': "'*'",
        'method.response.header.Access-Control-Allow-Credentials': "'false'",
        'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
      },
    }],
    passthroughBehavior: apigateway.PassthroughBehavior.Never,
    requestTemplates: {
      "application/json": "{\"statusCode\": 200}"
    },
  }))
  const methodResource = options.findChild('Resource') as apigateway.cloudformation.MethodResource
  methodResource.propertyOverrides.methodResponses = [{
    statusCode: '200',
    responseModels: {
      'application/json': 'Empty'
    },
    responseParameters: {
      'method.response.header.Access-Control-Allow-Headers': true,
      'method.response.header.Access-Control-Allow-Methods': true,
      'method.response.header.Access-Control-Allow-Credentials': true,
      'method.response.header.Access-Control-Allow-Origin': true,
    },
  }]
}

@rix0rrr rix0rrr added feature-request A feature should be added or improved. and removed feature-request A feature should be added or improved. feature labels Nov 6, 2018
@FinnIckler
Copy link

I am also currently struggling with this. How do you actually use the provided code snippet? Add it to the root of the api? Or for every Resource I want to enable CORS for? I currently have something like:

const blog_api = new apigw.RestApi(this,"Blog-Rest-Api");
const blogs =  blog_api.root.addResource('blogs');

const blog  = blogs.addResource('{blog_id}');
const addBlog = blogs.addResource('add');

// A get to blogs just returns all blogs
const getBlogsIntegration = new apigw.LambdaIntegration(getBlogsHandler);
blogs.addMethod('GET',getBlogsIntegration);

// A get to blogs/id gets only the one blog with the specific id 
const getBlogIntegration = new apigw.LambdaIntegration(getBlogHandler);
blog.addMethod('GET',getBlogIntegration);

With what variables to I have to call addCorsOptions and at which point? I tried on the root and on blogs, but couldn't get it to work yet.

@kennu
Copy link

kennu commented Nov 7, 2018

I have attached it to the API root and any resource paths like this:

    const api = new apigateway.RestApi(this, 'Api', { ... })
    addCorsOptions(api.root)

    const apiContacts = api.root.addResource('contacts')
    addCorsOptions(apiContacts)

You should be able to verify after deployment in API Gateway Console that everything is present.

But you also need to return an Access-Control-Allow-Origin header from your Lambda, because API Gateway doesn't add that automatically to the responses. The addCorsOptions() function here only adds a separate OPTIONS method and its headers.

(I do wish API Gateway could handle all of this automagically with a single enable option.. It's been very complicated from the beginning.)

@debora-ito debora-ito added the @aws-cdk/aws-apigateway Related to Amazon API Gateway label Nov 7, 2018
@FinnIckler
Copy link

Thanks, that worked!

@rix0rrr rix0rrr added the gap label Jan 4, 2019
@piotrkubisa
Copy link

piotrkubisa commented Jan 18, 2019

Example provided by @kennu stopped working for the latest version of the CDK. However, after some changes following code is working for me:

function addCorsOptions(apiResource: apigateway.IRestApiResource) {
  const options = apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
    integrationResponses: [{
      statusCode: '200',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
        'method.response.header.Access-Control-Allow-Origin': "'*'",
        'method.response.header.Access-Control-Allow-Credentials': "'false'",
        'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
      },
    }],
    passthroughBehavior: apigateway.PassthroughBehavior.Never,
    requestTemplates: {
      "application/json": "{\"statusCode\": 200}"
    },
  }))
  const methodResource = (options as cdk.Construct).node.findChild("Resource") as apigateway.CfnMethod
  methodResource.propertyOverrides.methodResponses = [{
    statusCode: '200',
    responseModels: {
      'application/json': 'Empty'
    },
    responseParameters: {
      'method.response.header.Access-Control-Allow-Headers': true,
      'method.response.header.Access-Control-Allow-Methods': true,
      'method.response.header.Access-Control-Allow-Credentials': true,
      'method.response.header.Access-Control-Allow-Origin': true,
    },
  }]
}

As a hint if you get 500 status on OPTION methods is to set following in the MockIntegration definition (it is a common error if you are using compression over API Gateway):

contentHandling: apigateway.ContentHandling.ConvertToText

@wmunyan
Copy link

wmunyan commented Jan 18, 2019

Hello, I am trying to implement something similar, but in Java. I unfortunately cannot get past the const methodResource = (options as cdk.Construct).node.findChild("Resource") as apigateway.CfnMethod piece of this code.

The Java implementation is not letting me cast the cdk.Construct to an apigateway.CfnMethod.

Any Java experts out there who might be able to lend a hand?

Thanks in advance!

@piotrkubisa
Copy link

I can't help but I am looking forward to #1572 be resolved and merged.

@wmunyan
Copy link

wmunyan commented Jan 19, 2019

@piotrkubisa thank you for the note and reference to the issue. I'll be keeping an eye out for that issue as well!

@justinThompson
Copy link

The propertyOverrides.methodResponses option no longer works after the refactor: readonly struct properties and hide internals PR.

I'm not really sure how to work around this now.

Any ideas?

@kennu
Copy link

kennu commented Apr 7, 2019

I think you can now specify the method responses as an option to addMethod()

export function addCorsOptions(apiResource: apigateway.IRestApiResource) {
  apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
    integrationResponses: [{
      statusCode: '200',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
        'method.response.header.Access-Control-Allow-Origin': "'*'",
        'method.response.header.Access-Control-Allow-Credentials': "'false'",
        'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
      },
    }],
    passthroughBehavior: apigateway.PassthroughBehavior.Never,
    requestTemplates: {
      "application/json": "{\"statusCode\": 200}"
    },
  }), {
    methodResponses: [{
      statusCode: '200',
      responseParameters: {
        'method.response.header.Access-Control-Allow-Headers': true,
        'method.response.header.Access-Control-Allow-Methods': true,
        'method.response.header.Access-Control-Allow-Credentials': true,
        'method.response.header.Access-Control-Allow-Origin': true,
      },  
    }]
  })
}

@justinThompson
Copy link

Oh nice one, thanks 👍

@sankeyraut
Copy link

This is not working. Broken in 0.37.0 ?

@eladb
Copy link
Contributor Author

eladb commented Jul 7, 2019

What is not working? What is the error you are seeing?

@hvital
Copy link

hvital commented Jul 9, 2019

Just a few edits:

  • Rename IRestApiResource to IResource
  • Rename apigateway.PassthroughBehavior.Never to apigateway.PassthroughBehavior.NEVER
export function addCorsOptions(apiResource: apigateway.IResource) {
    apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
        integrationResponses: [{
        statusCode: '200',
        responseParameters: {
            'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
            'method.response.header.Access-Control-Allow-Origin': "'*'",
            'method.response.header.Access-Control-Allow-Credentials': "'false'",
            'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
        },
        }],
        passthroughBehavior: apigateway.PassthroughBehavior.NEVER,
        requestTemplates: {
        "application/json": "{\"statusCode\": 200}"
        },
    }), {
        methodResponses: [{
        statusCode: '200',
        responseParameters: {
            'method.response.header.Access-Control-Allow-Headers': true,
            'method.response.header.Access-Control-Allow-Methods': true,
            'method.response.header.Access-Control-Allow-Credentials': true,
            'method.response.header.Access-Control-Allow-Origin': true,
        },  
        }]
    })
}

@sankeyraut
Copy link

Works like a charm

@eladb eladb self-assigned this Aug 12, 2019
@vegetabill
Copy link

Just curious why it's MockIntegration. It feels weird to put that in a production config

@ghost
Copy link

ghost commented Aug 28, 2019

@eladb I'm using LambdaRestApi and when I try to add this - it simply remove the lambda integration.... how to add CORS in this case ?

const APIGW = new apigw.LambdaRestApi(this, 'ListBranchesAPIGW', {
handler: FUNCTION,
apiKeySourceType: apigw.ApiKeySourceType.HEADER,
restApiName: 'ListBranchesAPI',
deployOptions: { stageName: 'prod' },
defaultMethodOptions: { apiKeyRequired: true },
proxy: true
});
addCorsOptions(APIGW.root)

@ghost
Copy link

ghost commented Aug 29, 2019

thanks every one on this thread for the code samples. Finally, I successfully, deployed an API Gateway + Lambda Proxy with COGNITO Authorizer - sharing the code - might help someone who lands here..

    // construct: lambda
    const FUNCTION = new lambda.Function(this, 'ListBranchesHandler', {
      runtime: lambda.Runtime.NODEJS_10_X,
      code: lambda.Code.asset('lib/list-branches/function'), // your function directory
      handler: 'list-branches.handler'
    });
    // construct: apigw
    const APIGW = new apigw.LambdaRestApi(this, 'ListBranchesAPIGW', {
      handler: FUNCTION, // attaching lambda function
      apiKeySourceType: apigw.ApiKeySourceType.HEADER,
      restApiName: 'ListBranchesAPI',
      deployOptions: { stageName: 'prod' },
      defaultMethodOptions: {
        apiKeyRequired: false
      },
      proxy: false
    });
    // --> defining an authorizer COGNITO
    const authorizer = new CfnAuthorizer(this, 'CognitoAuth', {
      restApiId: APIGW.restApiId.toString(),
      type: apigw.AuthorizationType.COGNITO,
      identitySource: 'method.request.header.Authorization',
      name: 'CognitoAuthName',
      providerArns: ['ARN OF COGNITO USER POOL']
    })
    // --> defining an authorizer COGNITO
 const resource = APIGW.root.addResource('branches') // creating a resource
    addCorsOptions(resource) // adding OPTION for CORS
    // --> adding the authorizer
    const method = resource.addMethod('GET', undefined, {
      // apiKeyRequired: true, // enable this if you need API key
      authorizationType: apigw.AuthorizationType.COGNITO,
      authorizer: {authorizerId: authorizer.ref}
    })
     // --> adding the authorizer
    function addCorsOptions(apiResource: apigw.IResource) {
      apiResource.addMethod('OPTIONS', new apigw.MockIntegration({
        integrationResponses: [{
          statusCode: '200',
          // contentHandling: apigw.ContentHandling.CONVERT_TO_TEXT,
          responseParameters: {
            'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
            'method.response.header.Access-Control-Allow-Origin': "'*'",
            'method.response.header.Access-Control-Allow-Credentials': "'false'",
            'method.response.header.Access-Control-Allow-Methods': "'GET,OPTIONS'", // modify this based on methods
          }
        }],
        passthroughBehavior: apigw.PassthroughBehavior.WHEN_NO_MATCH,
        requestTemplates: {
          "application/json": "{\"statusCode\": 200}"
        }
      }), {
        methodResponses: [{
          statusCode: '200',
          responseParameters: {
            'method.response.header.Access-Control-Allow-Headers': true,
            'method.response.header.Access-Control-Allow-Methods': true,
            'method.response.header.Access-Control-Allow-Credentials': true, // COGNITO
            'method.response.header.Access-Control-Allow-Origin': true,
          }

        }]
      })
    }

@ghost ghost mentioned this issue Aug 29, 2019
12 tasks
@gadams999
Copy link

gadams999 commented Sep 3, 2019

thanks every one on this thread for the code samples. Finally, I successfully, deployed an API Gateway + Lambda Proxy with COGNITO Authorizer - sharing the code - might help someone who lands here..

// --> defining an authorizer COGNITO
const authorizer = new CfnAuthorizer(this, 'CognitoAuth', {
  restApiId: APIGW.restApiId.toString(),
  type: apigw.AuthorizationType.COGNITO,
  identitySource: 'method.request.header.Authorization',
  name: 'CognitoAuthName',
  providerArns: ['ARN OF COGNITO USER POOL']
})

@smfontevan, this is super helpful. I have a CFn stack where I assign a function to each resource's method (or at least to each resource), but the [LambdaRestApi] construct (https://docs.aws.amazon.com/cdk/api/latest/python/aws_cdk.aws_apigateway/LambdaRestApi.html) wasn't getting me there. I don't work with API Gateway a lot, so seeing your JS code was helpful. While converting to Python, the Cognito authorizer above wasn't working. I noticed in the CloudFormation definition that type should be TOKEN, 'COGNITO_USER_POOLS, or 'REQUEST.

Haven't fully tested yet (waiting on CloudFront distro delete/create), but this has helped understand the non-swagger/AWS:SERVERLESS was of creating an API gateway.

EDIT: code formatting

@eladb eladb assigned nija-at and unassigned eladb Sep 3, 2019
@eladb
Copy link
Contributor Author

eladb commented Sep 20, 2019

@eladb
Copy link
Contributor Author

eladb commented Sep 21, 2019

We are planning to implement this feature on a live Twitch stream next week: https://www.twitch.tv/events/4tjzcuGeSymHA5UCn-MbrA

Would love to have you guys over for real time feedback and inputs!

@spaceemotion
Copy link

It would be great if the API would allow modifying the origins (and maybe methods?) to support the following setup:

  • the backend at api.site.com only allows access for
  • the SPA frontend at app.site.com

@rix0rrr
Copy link
Contributor

rix0rrr commented Sep 23, 2019

The OPTIONS request can (should?) return HTTP/1.1 204 No Content instead of a bogus JSON response.

@eladb
Copy link
Contributor Author

eladb commented Sep 23, 2019

@spaceemotion not sure I understand the requirement. Wouldn't that just mean specifying Allow-Origin: https://app.site.com?

@spaceemotion
Copy link

spaceemotion commented Sep 23, 2019

Yes. I just thought I'd mention it since you said you'd want to implement the feature next/this week. Not sure what that entails, but setting the origin via props would be nice. Setting it by hand using the methods described above works like a charm.

@eladb
Copy link
Contributor Author

eladb commented Sep 23, 2019

Cool, yeah. Obvsiouly users should be able to specify the allowed origin(s).

@SomayaB SomayaB added management/tracking Issues that track a subject or multiple issues and removed gap labels Sep 24, 2019
@mergify mergify bot closed this as completed in #4211 Oct 17, 2019
@moravcik
Copy link

moravcik commented Feb 14, 2020

For anybody interested, I managed to setup Gateway response for custom authorizer (when access is denied) with CfnGatewayResponse:

new CfnGatewayResponse(this, 'AccessDeniedResponse', {
  responseType: 'ACCESS_DENIED',
  restApiId: api.restApiId, // reference to RestApi created earlier
  statusCode: '403',
  responseParameters: {
    'gatewayresponse.header.x-my-custom-header': "'my-custom-header-value'",
  },
  responseTemplates: {
    'application/json': '{"message":"CUSTOM_ERROR_MSG"}'
  }
});

The only problem I have with this solution is that changes to CfnGatewayResponse are not automatically deployed with cdk deploy, I have to press manually "Deploy API" in AWS Console.

@connorjs
Copy link

(1) What is the recommended way of enabling CORS pre-flight for APIG + Lambda proxy integration with the latest CDK? (I am using 1.33.0.)

It seems that defaultCorsPreflightOptions is a prop on ResourceOptions (a parent of LambdaRestApiProps), which I think neatly adds everything.

const api = new LambdaRestApi(this, "Api", {
  defaultCorsPreflightOptions: {
    allowOrigins: ["https://example.com"]
  },
  // ...other props
});

I think the answer to (1) is, "Yes, use defaultCorsPreflightOptions".


(2) However, does the recommendation change when an authorization type is added? As shown below, I have included IAM authorization which is also applied to the OPTIONS method. Unfortunately, this violates the CORS pre-flight contract or, at least, guarantees it will fail b/c the pre-flight OPTIONS request will not have credentials (per my understanding).

const api = new LambdaRestApi(this, "Api", {
  defaultCorsPreflightOptions: {
    allowOrigins: ["https://example.com"],
    allowCredentials: true
  },
  defaultMethodOptions: {
    authorizationType: AuthorizationType.IAM
  },
  // ...other props
});

Is there a way to "remove" the authorization type for a specific method (or set to NONE). More generally speaking, can the methods of a REST resource be mutated post-construction?

Alternatively, is there ever a valid case to restrict OPTIONS? (I'm still new to this, but I only see CORS pre-flight mentioned on MDN's OPTIONS doc.) If there are not use cases, can we update the APIG construct to not apply (ignore) the authorization type to OPTIONS methods?

In the mean-time, I'm going to continue use the hand-rolled solution. Thanks for the details on this thread so far everyone.

@leantorres73
Copy link

I'm having the same issue, in SAM this was fixed while ago with a flag AddDefaultAuthorizerToCorsPreflight.
aws/serverless-application-model#958

@leantorres73
Copy link

@connorjs What's the hand-rolled solution you're talking about? I tried the @smfontevan solution with no success (I'm still having CORS issues)

@eladb
Copy link
Contributor Author

eladb commented May 10, 2020

@leantorres73 @connorjs can you please raise a new issue so we can track this properly and get it fixed. It's harder for us to track as comments on a closed issue.

@leantorres73
Copy link

@connorjs I have Cognito + API Gateway + Proxy integration + CORS working. It's not a real proxy anymore, but until there is an easy solution to disable security in OPTIONS, this is a way to go

    const proxy = api.root.addProxy({
      defaultIntegration: new apigateway.LambdaIntegration(handler),
      anyMethod: false
    });
    const authorizerConfig = {
      authorizer: {
        authorizerId: authorizer.ref
      },
      authorizationType: apigateway.AuthorizationType.COGNITO
    };
    proxy.addMethod('GET', undefined, authorizerConfig);
    proxy.addMethod('POST', undefined, authorizerConfig);
    proxy.addMethod('PUT', undefined, authorizerConfig);
    addCorsOptions(proxy);

@eladb
Copy link
Contributor Author

eladb commented May 19, 2020

@connorjs I have Cognito + API Gateway + Proxy integration + CORS working. It's not a real proxy anymore, but until there is an easy solution to disable security in OPTIONS, this is a way to go

    const proxy = api.root.addProxy({

      defaultIntegration: new apigateway.LambdaIntegration(handler),

      anyMethod: false

    });

    const authorizerConfig = {

      authorizer: {

        authorizerId: authorizer.ref

      },

      authorizationType: apigateway.AuthorizationType.COGNITO

    };

    proxy.addMethod('GET', undefined, authorizerConfig);

    proxy.addMethod('POST', undefined, authorizerConfig);

    proxy.addMethod('PUT', undefined, authorizerConfig);

    addCorsOptions(proxy);

Can you please a separate GitHub issue on this?

@aws aws locked and limited conversation to collaborators May 19, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
@aws-cdk/aws-apigateway Related to Amazon API Gateway feature-request A feature should be added or improved. management/tracking Issues that track a subject or multiple issues
Projects
None yet
Development

Successfully merging a pull request may close this issue.