serverless-application-model: Cannot set DefaultAuthorizer and have CORS enabled
Description:
I want to deploy an API Gateway that both has a custom lambda authorizer and uses CORS. However, the configuration always ends up in a non-working state. Because of #650 , the only authorizer you can specify is the DefaultAuthorizer (if you are referencing a swagger at all). This means the authorizer is applied to every single endpoint in the API - including OPTIONS endpoints generated by setting CORS. However, CORS methods are not meant to be authorized - particularly if you are using Headers authorization, since the Authorization
header is stripped out from CORS pre-flight checks. Which leaves us with the following options:
- Do not authorize our API. (Obviously cannot do this)
- Do not enable CORS (Also cannot do this, as we must allow or web application to talk with our API)
- Manually, in the AWS console, remove the IdentitySource for the authorizer in the API Gateway after every single automated deployment (not sustainable or practical)
- Manually, in the AWS Console, remove the authorizer from every single OPTIONS endpoint (also not sustainable or practical)
I understand why IdentitySource is required in the serverless template and in aws cli (which begs the question why it can be removed in AWS Console at all), but because #650 is not fixed, we cannot manually associate the authorizer with our explicit endpoints - thereby leaving the generated CORS endpoints unauthorized. Therefore, one cannot have custom authorizers and CORS enabled on an API Gateway
Steps to reproduce the issue:
- Create a serverless template that enables CORS, creates a custom authorizer (with Header
IdentitySources
), sets that authorizer as theDefaultAuthorizer
, and uses an inlineDefinitionBody
Example template:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Users lambda functions
Parameters:
EnvironmentName:
Type: String
Description: 'Enter: dev, stage, or prod'
LambdaRole:
Type: String
Default: 'arn:aws:iam::[PII]:role/api-lambda-role'
Globals:
Function:
CodeUri: ''
Runtime: dotnetcore2.1
MemorySize: 256
Timeout: 30
Resources:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref EnvironmentName
Cors:
AllowOrigin: "'*'"
AllowHeaders: "'*'"
Auth:
DefaultAuthorizer: ApiAuthorizer
Authorizers:
ApiAuthorizer:
FunctionPayloadType: REQUEST
FunctionArn: !GetAtt Authorize.Arn
Identity:
Headers:
- Authorization
ReauthorizeEvery: 0
DefinitionBody:
swagger: 2.0
info:
title:
Fn::Sub: "api-${EnvironmentName}"
version: "1.0"
servers:
- url: "https://api-${EnvironmentName}.example.com/{basePath}"
variables:
basePath:
default:
Fn::Sub: "/${EnvironmentName}/v1"
paths:
/user/{id}:
get:
x-amazon-apigateway-integration:
uri:
Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetUser.Arn}/invocations"
passthroughBehavior: "when_no_match"
httpMethod: "POST"
type: "aws_proxy"
GatewayResponseDefault4XX:
Type: 'AWS::ApiGateway::GatewayResponse'
Properties:
ResponseParameters:
gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
ResponseType: DEFAULT_4XX
RestApiId: !Ref ApiGatewayApi
Authorize:
Type: AWS::Serverless::Function
Properties:
Handler: GlobalEditAPI.Users::GlobalEditAPI.Users.Functions.AuthorizerFunctions::Authorize
Role: !Ref LambdaRole
GetUser:
Type: AWS::Serverless::Function
Properties:
Handler: GlobalEditAPI.Users::GlobalEditAPI.Users.Functions.UserFunctions::GetUserAsync
Role: !Ref LambdaRole
Events:
ProxyApi:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /user/{id}
Method: GET
Outputs:
ApiUrl:
Description: URL of Users API endpoint
Value: !Join
- ''
- - https://
- !Ref ApiGatewayApi
- '.execute-api.'
- !Ref 'AWS::Region'
- '.amazonaws.com/'
- !Ref EnvironmentName
- Deploy the template to AWS
(We are using dotnet, but underneath it is basic AWS CLI)
dotnet lambda deploy-serverless --s3-bucket "api-example" --stack-name "api-example" --template-parameters EnvironmentName="example"
Observed result:
The authorizer has been associated with the OPTIONS
endpoints, which will cause them to fail.
Expected result:
I should be able to either:
- Not have authorizers associated with “generated” endpoints from CORS, even if the authorizer is set a Default
- Have #650 fixed so I can manually associate authorizers with endpoints, thereby forcing the authorizer to not associate with the generated CORS endpoints
- Have Authorizers in API Gateway automatically give OPTIONS requests a pass and do not try to authorize them
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 37
- Comments: 27 (7 by maintainers)
Commits related to this issue
- Implement OPTIONS method to make CORS Preflight work see: https://github.com/awslabs/serverless-application-model/issues/717 — committed to superluminar-io/serverless-workshop-py by tillkahlbrock 5 years ago
@richiewebgate Under Api -> Auth. I’m using it also in code pipeline so it should work for you too.
@cidthecoatrack I have been dealing with the same problem, but I found a fifth option: Use a macro to remove the authorizer from options paths. It took a bit of stumbling, but I’ll share:
For your existing template, we need to add another transformation. I’m going to call the transformation CorsFixer. To apply this transformation change the line:
to:
Transforms are run in order, so it will run after the Serverless transform has applied the Authorizers and Cors. Sadly, macros have to be defined in a separate stack from where they are used. Here’s how I accomplished that:
The javascript here is pretty simple. It finds all of the RestApi resources, then searches for all options requests and overwrites any authorization w/ “NONE”. I’m not a cloudformation expert, but this should work in the majority of cases. Package and deploy that to a dedicated stack, then package and deploy your normal stack and you should be good to go!
One note: when I first set this up, ApiGateway got the changes (authorization disabled) but it doesn’t seem like it deployed. I manually deployed and everything worked great. Since then, I’ve done multiple deployments via SAM and everything has worked great.
Hope this helps!
There is an example in https://github.com/awslabs/serverless-application-model/pull/1079 that shows how to use this feature.
thanks a lot @leantorres73 and @praneetap. I tried it right now and it finally works!! YAY so cool!!
Hi, The PR Bugfix: CORS Security #828 seems to be accepted. The “Transform” : “AWS::Serverless-2016-10-31” does not apply with it (obviously) . Where I could found the latest availables Transfom template list to use ?
I was able to get a 6th workaround, though it is not as great as the macro option and I am going to try that next. Also, we don’t use swagger in our serverless templates, that may make a difference, given #650. But the basic idea is that you specify 2 events in the function for each path you want to authorize: one event uses
Method: GET
, the other usesMethod: OPTIONS
:This is working for me when I have a
DefaultAuthorizer
set. If you don’t set aDefaultAuthorizer
, then you would have to explicitly opt each non-OPTIONS endpoint in usingAuth: Authorizer: ApiAuthorizer
, but then the OPTIONS mocks that get deployed will have Auth: NONE (because they do not inherit the default authorizer from the api).Python isn’t a language I’ve used much but I am working on fixing this and adding unit tests to detect regressions. It looks like
flipping the order CORS and auth are added to the API Gateway andadding a security value to the OPTIONS endpoints added for CORS should handle it.I’m seeing the same the same issue.
The only workaround I have found that doesn’t require macros or other additional implementation is to not set a DefaultAuthorizer but to apply they Authorizer on each resource instead, when doing it this way OPTIONS will not be authorized but instead falls back to NONE as expected.
OPTIONS requests should not be authorized at all, regardless of what the DefaultAuthorizer is set to. Most browsers will not send the Authentication header unless explicitly instructed to do which means that authorizing OPTIONS requests won’t work at all for the most part.
Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
This also relates to #815 since GatewayResponses are required to fully handle CORS on requests where the Authorizer denies the request.
in which section of the SAM template should the new option “AddDefaultAuthorizerToCorsPreflight” be placed? I tried several combinations and couldn’t get the expected results.
… or is this still not released yet? (I am using codepipeline)
thanks
Sorry for being late to the party. There are valid use cases for adding Auth to preflight. I have provided @jbutz with the following comment on their PR #828, and then once everything is ready we can get that merged in.
| I don’t think we should be adding this in every scenario. Please add an additional property to the
Auth
property calledAddDefaultAuthorizerToCorsPreflight
. This property should have a default value ofTrue
so that we don’t break anyone relying on this functionality.@cidthecoatrack Thanks for posting this. Just spend a day trying to figure out if I did something wrong. Having the options require authorization does not make sense. I think this is CF thing. I wonder if there is different way to configure this in CF template.