How to solve SAM 403 Errors on CORS Preflight

The answer might be easier than you think: your OPTIONS method might be requiring authentication
API Gateway
SAM
CORS
CloudFormation
Picture of John Wright Stanly, author of this blog article
John Wright Stanly
Mar 20, 2021

-

-

If your browser's CORS preflight request is responding with a 403, your API's OPTIONS method could be requiring authentication. This might sound okay if your API is authenticated, but the OPTIONS method behaves differently than other HTTP methods. Your API server will need to comply with the CORS standard, which requires OPTIONS methods to work without authentication headers.

How do CORS preflight requests work?

Image

For non-simple HTTP requests, your browser sends a preflight request to check if the server complies with the browser's CORS configuration. The browser completes this preflight with an OPTIONS request to the endpoint with these three headers: Access-Control-Request-Method, Access-Control-Request-Headers, and Origin. The browser does this itself automatically; developers can't manually send the OPTIONS request on the behalf of the browser.

Therefore, it's not possible to add extra headers like x-api-key. If your API is designed to require an API key for OPTIONS methods, it's guarenteed to respond with a 403, even if the actual HTTP request includes the API key in a x-api-key header.

Fixing your API Gateway configuration

Remove any authenticaion requirements found in your OPTIONS method(s).

If you're using the AWS Serverless Application Model (SAM), you know how awesome it can be for creating API's. It can automatically handle CORS and authentication all with just a couple lines of a SAM template.

However, make sure to NOT declare ApiKeyRequired: true inside the Auth property of AWS::Serverless::API. This will require an API key for all methods, including the OPTIONS methods SAM automatically creates to handle CORS.

SampleApi:
  Type: AWS::Serverless::Api
  Properties:
    StageName: Prod
    Auth:
      ApiKeyRequired: true #  <-- BAD; CAUSES 403's ON OPTIONS
      UsagePlan:
        CreateUsagePlan: PER_API
        Description: Sample usage plan
        Throttle:
          BurstLimit: 10000
          RateLimit: 5000
    Cors:
      AllowMethods: "'*'"
      AllowHeaders: "'*'"
      AllowOrigin: !Sub
        - "'https://www.${Domain}'"
        - Domain: !Ref DomainName
    Domain:
      DomainName: !Join ['.', [api, !Ref DomainName]]
      CertificateArn: !Ref AcmCertificateArn
      EndpointConfiguration: EDGE
      Route53:
        HostedZoneId: !Ref HostedZone

If you want to build an API with SAM that requires an API key for every method, manually define the API key requirement in every AWS::Serverless::Function definition under it's Events property.

SampleFunction:
  Type: AWS::Serverless::Function
  Properties:
    Handler: handlers/index.sampleFunction
    Description: This is a sample
    Policies:
      - AWSLambdaBasicExecutionRole
    Events:
      Api:
        Type: Api
        Properties:
          RestApiId: !Ref SampleAPI
          Path: /endpoint
          Method: GET
          Auth:
            ApiKeyRequired: true #  <-- HERE

Currently, there is no way with SAM to globally require API keys and still satisfy CORS. You also can't standardize API key requirements in the Globals template section because the Events property needs to be unique to every function. Although this might seem like a hassle, authenticating an API with only API keys is not advised. AWS offers plenty other authentication solutions that are more robust. If you're experiencing this problem and only authenticating your API with API keys, it might be time investigate in a beefier authorization solution, such as IAM's or a Lambda authorizer.

Comments

Be the first to add a comment!

Add Comment

Post