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

Cannot set DefaultAuthorizer and have CORS enabled #717

Closed
cidthecoatrack opened this issue Dec 12, 2018 · 27 comments
Closed

Cannot set DefaultAuthorizer and have CORS enabled #717

cidthecoatrack opened this issue Dec 12, 2018 · 27 comments

Comments

@cidthecoatrack
Copy link

cidthecoatrack commented Dec 12, 2018

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:

  1. Do not authorize our API. (Obviously cannot do this)
  2. Do not enable CORS (Also cannot do this, as we must allow or web application to talk with our API)
  3. Manually, in the AWS console, remove the IdentitySource for the authorizer in the API Gateway after every single automated deployment (not sustainable or practical)
  4. 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:

  1. Create a serverless template that enables CORS, creates a custom authorizer (with Header IdentitySources), sets that authorizer as the DefaultAuthorizer, and uses an inline DefinitionBody

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
  1. 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 When using an inline swagger, api+method+path authorizers do not get added  #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
@danielgranat
Copy link

@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.

@disciplezero
Copy link

@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:

Transform: AWS::Serverless-2016-10-31

to:

Transform: ["AWS::Serverless-2016-10-31", "CorsFixer"]

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:

AWSTemplateFormatVersion: 2010-09-09
Resources:
  TransformExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: [lambda.amazonaws.com]
            Action: ['sts:AssumeRole']
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: ['logs:*']
                Resource: 'arn:aws:logs:*:*:*'
  TransformFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          exports.handler = async (event) => {
            for(k in event.fragment.Resources) {
              const res = event.fragment.Resources[k];
              if (res.Type !== 'AWS::ApiGateway::RestApi') continue;
              for(p in res.Properties.Body.paths) {
                const path = res.Properties.Body.paths[p];
                if(typeof path.options === 'undefined') continue;
                path.options.security = [{NONE:[]}]
              }
            }
            const response = {
              requestId: event.requestId,
              status: 'success',
              fragment: event.fragment,
            };

            return response;
          };
      Handler: index.handler
      Runtime: nodejs8.10
      Role: !GetAtt TransformExecutionRole.Arn
  TransformFunctionPermissions:
    Type: AWS::Lambda::Permission
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !GetAtt TransformFunction.Arn
      Principal: 'cloudformation.amazonaws.com'
  Transform:
    Type: AWS::CloudFormation::Macro
    Properties:
      Name: 'CorsFixer'
      Description: Fixes Cors Headaches in CloudFormation
      FunctionName: !GetAtt TransformFunction.Arn

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!

@danludwig
Copy link

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 uses Method: OPTIONS:

  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
        ProxyApiCors:
          Type: Api
          Properties:
            RestApiId: !Ref ApiGatewayApi
            Path: /user/{id}
            Method: OPTIONS
            Auth:
              Authorizer: NONE

This is working for me when I have a DefaultAuthorizer set. If you don't set a DefaultAuthorizer, then you would have to explicitly opt each non-OPTIONS endpoint in using Auth: 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).

@guijob
Copy link

guijob commented Feb 6, 2019

@danludwig, your workaround actually calls related lambda function with OPTIONS method, so you'll have to handle preflight request into your code. So, I suggest @disciplezero workaround which doesn't call any code, I use it and worked like a charm.

@danludwig
Copy link

@guijob yes agreed! We still need the workaround when debugging with auth enabled on the endpoint locally, but we strip out the options events before committing so that code never gets hit after deployed with the macro transform.

@carlnordenfelt
Copy link
Contributor

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

The most interesting capability exposed by both XMLHttpRequest or Fetch and CORS is the ability to make "credentialed" requests that are aware of HTTP cookies and HTTP Authentication information. By default, in cross-site XMLHttpRequest or Fetch invocations, browsers will not send credentials. A specific flag has to be set on the XMLHttpRequest object or the Request constructor when it is invoked.

This also relates to #815 since GatewayResponses are required to fully handle CORS on requests where the Authorizer denies the request.

@jbutz
Copy link

jbutz commented Feb 27, 2019

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 and adding a security value to the OPTIONS endpoints added for CORS should handle it.

@jbutz jbutz mentioned this issue Feb 28, 2019
3 tasks
@brettstack
Copy link
Contributor

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 called AddDefaultAuthorizerToCorsPreflight. This property should have a default value of True so that we don't break anyone relying on this functionality.

@brettstack brettstack added the contributors/good-first-issue Good first issue for a contributor label Mar 1, 2019
@rsc-wgc
Copy link

rsc-wgc commented Apr 12, 2019

It would be really nice if there is a simple flag to prevent the authorizer to be set on the OPTIONS method, without workarounds and stuff. The OPTIONS preflight call is usually made automatically by the browser, not by the application code. If you use an HTTPInterceptor to add Authorization headers to each outgoing request of the webapp, the OPTIONS preflight requests are not intercepted and the API cannot be used because the preflight fails due to missing authorization. Thanks

@tsuga
Copy link

tsuga commented May 2, 2019

I agree with @richiewebgate

@brettstack
Copy link
Contributor

We are happy to work with anyone interested in submitting a PR for this option.

@stavros-zavrakas
Copy link

I was struggling quite a lot today with this and I've ended up to remove completely the cors from the Globals definition and add at the very end of my resources another lambda function, which registers on ANY (you might want to restrict it into OPTIONS) different method on the /{proxy+} path and the only job that is doing is to handle all the requests that are not defined on the resources above it, return 204 and the Access-Control-Allow-Origin header. This is my definition:

  CorsFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Join [ "-", [!Ref Environment, cors] ]
      MemorySize: 128
      Timeout: 30
      Role: 
        Fn::GetAtt: 
          - "LambdaRole"
          - "Arn"
      CodeUri: resources/cors/
      Handler: cors.allow
      Layers:
        - !Ref DatabaseDependencyLayer
      Runtime: nodejs8.10
      Events:
        ProxyApiGreedy:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY
            Auth:
              Authorizer: NONE
            RestApiId: !Ref ApiGateway
      Environment:
        Variables:
          environment: !Ref Environment

@fcasals
Copy link

fcasals commented May 31, 2019

Nice alternative @stavros-zavrakas, I'll try that solution next week.
I'm currently using the solution posted by @disciplezero with some minor changes and excellent results.

@brettstack
Copy link
Contributor

This should be a pretty simple addition if someone wants to work on this. #828 has the bulk of the work; I'm asking for an additional property under API Auth called AddDefaultAuthorizerToCorsPreflight with a default value of True (for backwards compatibility).

@leantorres73
Copy link

leantorres73 commented Jun 12, 2019

I can't make the Macro work @disciplezero

FAILED - Failed to execute transform ACCOUNT-NUMBER::CorsFixer

Any suggestions?

@disciplezero
Copy link

@leantorres73 - Not an expert in CloudFormation, so the error doesn't give me any insights. We're using this in production, but I basically haven't touched it since we launched that template.

My first guess is that you didn't install the CorsFix macro. In your template when you say:

Transform: ["AWS::Serverless-2016-10-31", "CorsFixer"]

It has to find those transforms. Not sure how it resolves the AWS one, but CorsFixer isn't publicly available. You have to deploy that template in your own account. The template I posted above I have saved as macros.yml. My Makefile has a deploy target for this, but you should be able to do something like:

sam package --output-template-file cf-macros.yml --s3-bucket $(CLOUDFORMATION_BUCKET) --template-file macros.yml
sam deploy --capabilities CAPABILITY_NAMED_IAM --template-file cf-macros.yml --stack-name $(STACK_NAME)-macros

Then I would try again. If that doesn't help - you might need to look at CloudWatch. The TransformFunction in the macros.yml is just a lambda function. It will have the normal CloudWatch logs and they may surface insights. Ours is named something like /aws/lambda/<stackname-macros-TransformFunction-<someweirdstring>

In normal operation you should see entries in there for when the lambda starts/stops. No other content.

Hope that helps!

@leantorres73
Copy link

leantorres73 commented Jun 12, 2019

Hi @disciplezero, thanks for the help. I did everything you said but it's still not working for me. The lambda is never invoked.
I'm having issues in the pipeline where I include the CorsFixer:
image

Checking the Cloudformation log it appears that message.
FAILED - Failed to execute transform ACCOUNT-NUMBER::CorsFixer

This is weird because it shouldn't fail if the CorsFixer was created (and it was, I looked into Cloudformation and the macro deploy was successful).

I'll continue digging to find what could be wrong.

Thanks!

@danipenaperez
Copy link

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 ?

@praneetap
Copy link
Contributor

Released with v1.13.0

@rsc-wgc
Copy link

rsc-wgc commented Aug 20, 2019

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

@leantorres73
Copy link

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

@richiewebgate Under Api -> Auth. I'm using it also in code pipeline so it should work for you too.

image

@praneetap
Copy link
Contributor

There is an example in #1079 that shows how to use this feature.

@rsc-wgc
Copy link

rsc-wgc commented Aug 20, 2019

thanks a lot @leantorres73 and @praneetap. I tried it right now and it finally works!! YAY so cool!!

@AffeCode
Copy link

I have now been fiddling around with CORS, DefaultAuthorizer and AddDefaultAuthorizerToCorsPreflight: false for days, and I can't get it to work. I wanted to create a new issue, but it states that I should comment on an existing issue if there is one (open or closed), so here we go:

Here's a simplified version of my template:

Resources:
    myApiGateway:
        Type: AWS::Serverless::Api
        Properties:
            StageName: Staging
            Cors:
                AllowMethods: "'*'"
                AllowHeaders: "'*'"
                AllowOrigin: "'*'"
            Auth:
                Authorizers:
                    aadAuthorizer:
                        FunctionPayloadType: TOKEN
                        FunctionArn:
                            Fn::GetAtt:
                                - authorizerFunctionV1
                                - Arn
                DefaultAuthorizer: aadAuthorizer
                AddDefaultAuthorizerToCorsPreflight: false

It correctly adds my DefaultAuthorizer to all endpoints, but it also adds it to my generated OPTIONS endpoints, so I get a 401 when the browser makes preflight requests. If I call an OPTIONS endpoint from Postman with a bearer token, then I get the correct response, but this is obviously not possible from the browser.

It seems like a regression to the AddDefaultAuthorizerToCorsPreflight attribute that doesn't seem to work anymore.

Any thoughts? 😅

@ConnorRobertson
Copy link

@DK8ALREC
This code from when AddDefaultAuthorizerToCorsPreflight was added still seems to work. Perhaps looking at working example might give you some suggestions. If not can I get more information about the API?

Globals:
  Api:
    Cors: "origins"

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: s3://sam-demo-bucket/member_portal.zip
      Handler: index.gethtml
      Runtime: nodejs4.3
      Events:
        GetHtml:
          Type: Api
          Properties:
            Path: /
            Method: get
            RestApiId: !Ref ServerlessApi

        PostHtml:
          Type: Api
          Properties:
            Path: /
            Method: post
            RestApiId: !Ref ServerlessApi

  ServerlessApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Prod
      Auth:
        AddDefaultAuthorizerToCorsPreflight: False
        DefaultAuthorizer: MyLambdaRequestAuth
        Authorizers:
          MyLambdaRequestAuth:
            FunctionPayloadType: REQUEST
            FunctionArn: !GetAtt MyAuthFn.Arn
            Identity:
              Headers:
                - Authorization1

  MyAuthFn:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: s3://bucket/key
      Handler: index.handler
      Runtime: nodejs8.10

This person seems to be having a similar issue but using UserPool here.

@danipenaperez
Copy link

danipenaperez commented Jun 19, 2023

A workaround is only define the Authorizer, (disable global authorization (preflight too)), and on every Lambda define the authorizer explicity:

This is my working example (hope helps):

Resources:
  MyAPI:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Dev
      GatewayResponses: #https://stackoverflow.com/questions/36913196/401-return-from-an-api-gateway-custom-authorizer-is-missing-access-control-allo/58059560#58059560
        UNAUTHORIZED:
          StatusCode: 401
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"
        ACCESS_DENIED:
          StatusCode: 403
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"
        DEFAULT_5XX:
          StatusCode: 500
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"
        RESOURCE_NOT_FOUND:
          StatusCode: 404
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"              
      Cors: 
        AllowMethods: "'OPTIONS,GET,POST,PUT,DELETE'"
        AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key, Access-Control-Allow-Origin'"
        AllowOrigin: "'*'"
        #AllowCredentials: true
      Auth:
        # AddDefaultAuthorizerToCorsPreflight: False
        # DefaultAuthorizer: CustomLambdaTokenAuthorizer
        Authorizers:
          CustomLambdaTokenAuthorizer:
            FunctionArn: !GetAtt ValidateTokenFunction.Arn
            Identity:
              Header: Authorization
              #ValidationExpression: Bearer.*
  GetUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./app
      Handler: usersFacade.get
      Runtime: nodejs10.x
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          USERS_TABLE_NAME: !Ref UsersTable
      Events:
        GetUserPath:
          Type: Api
          Properties:
            RestApiId: !Ref MyAPI
            Path: /{apiVersionId}/users/{resourceId}
            Method: get
            Auth:
              Authorizer: CustomLambdaTokenAuthorizer 			
  UpdateUserFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./app
      Handler: usersFacade.updateUserInfo
      Runtime: nodejs10.x
      Policies: AmazonDynamoDBFullAccess
      Environment:
        Variables:
          USERS_TABLE_NAME: !Ref UsersTable
      Events:
        PutRoot:
          Type: Api
          Properties:
            RestApiId: !Ref MyAPI
            Path: /{apiVersionId}/users/{resourceId}/userInfo
            Method: put
            Auth:
              Authorizer: CustomLambdaTokenAuthorizer	
  ValidateTokenFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./app
      Handler: AuthorizerFacade.validateToken 
      Runtime: nodejs10.x
      Policies: 
        - AmazonDynamoDBFullAccess
        - SecretsManagerReadWrite 
      Environment:
        Variables:
          ACCESS_TOKEN_TTL: !Ref AccessTokenTTL
          REFRESH_TOKEN_TTL: !Ref RefreshTokenTTL
          SECRET: !Ref AuthSecret
          TOKEN_TABLE_NAME: !Ref AuthorizationTokenTable			  

@AffeCode
Copy link

@ConnorRobertson thanks for the reply. The only differences I can see between what I have tried and this is example is that FunctionPayloadType is set to REQUEST in the example while mine is set to TOKEN, and that the example is using Authorization1 instead of the default auth header. Did you run the example? Does OPTIONS requests receive a 200 when no auth header is provided?

@danipenaperez thanks, I already changed my API to use this solution. I just don't like the fact that auth has to be specified for each lambda, but that does seem to be the only way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests