Skip to content

Commit

Permalink
Normalize auth header to query string
Browse files Browse the repository at this point in the history
  • Loading branch information
symroe committed Apr 19, 2023
1 parent 4ef916f commit 6e02c9d
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ jobs:
SAM_CONFIG_FILE: samconfig.toml.d/ci-<<parameters.dc-environment>>.toml
SAM_LAMBDA_CONFIG_ENV: <<parameters.dc-environment>>
SAM_PUBLIC_CONFIG_ENV: <<parameters.dc-environment>>-public-access
SAM_LAMBDA_FUNCTION_CONFIG_ENV: <<parameters.dc-environment>>-cloudfront-lambda-functions
DC_DEPLOY_NAME: <<parameters.dc-deploy-name>>
POSTGRES_DATABASE_NAME: <<parameters.dc-deploy-name>>
steps:
Expand Down Expand Up @@ -134,6 +135,13 @@ jobs:
"
no_output_timeout: 30m
- run: pipenv run migratedb
- run:
name: "sam deploy lambda@edge function"
command: |
pipenv run sam deploy \
--config-file ~/repo/${SAM_CONFIG_FILE} \
--config-env $SAM_LAMBDA_FUNCTION_CONFIG_ENV
- run:
name: printenv PUBLIC_FQDN CERTIFICATE_ARN
# These envvars are stored inside CircleCI, which helpfully masks them if they're echoed.
Expand All @@ -157,6 +165,7 @@ jobs:
StackNameSuffix=<<parameters.dc-environment>> \
CertificateArn=$CERTIFICATE_ARN \
PublicFqdn=$PUBLIC_FQDN \
AuthHeaderToQueryStringARN=`python .circleci/get_cf_export_value.py AuthHeaderToQueryString` \
"
- run:
name: "Publish a new Sentry Release"
Expand Down
39 changes: 39 additions & 0 deletions .circleci/get_cf_export_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Because us-east-1 is really the only place that AWS works properly.
This shouldn't be a thing. But, it is a thing.
Our main stack is in `eu-west-2` and the CloudFront
distribution is "global", meaning a very specific part of the US.
However, when we want to attach a lambda@edge function
to the "global" resource for our `eu-west-2` deployment, the lambda
function itself needs to be in...you guessed it, a very specific part
of the US.
Fine, but we also need a way to share the version ARN of that function
with the CloudFront distribution. The right way to do this is via the
CloudFormation Exports system, however they don't work across regions.
This means that either Lambda@Edge only really works if you deploy to
a very specific part of the US, or if you hack some script together like
the below.
"""

import sys

import boto3


def get_export_value(export_name):
cf_client = boto3.client("cloudformation", region_name="us-east-1")
for export in cf_client.list_exports()["Exports"]:
if export["Name"] == export_name:
return export["Value"]
raise ValueError(f"Export {export_name} not found")


if __name__ == "__main__":
export_name = sys.argv[1]
print(get_export_value(export_name))
42 changes: 42 additions & 0 deletions api_endpoints/lambda_edge/auth_header_to_query/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from urllib.parse import parse_qs, urlencode


def lambda_handler(event, context):
request = event["Records"][0]["cf"]["request"]

"""
This code is run between a request from the web and that request
being passed on to CloudFront:
Request -> function -> CloudFront -> Origin
We use it to convert the `Authorization` header into a
query string parameter.
This is due to AWS API Gateway not being able to support
more than one authentication method using OR.
That is, you can add header and query string authentication methods, but
they are ANDed together meaning you need to supply both.
This function allows us to accept either by converting one method to using
the other method.
"""

# Check if the header exists
token_header = request["headers"].get("authorization")
if not token_header:
# If not, return early. Nothing to be done here
return request

# Parse request querystring to get dictionary/json
params = {k: v[0] for k, v in parse_qs(request["querystring"]).items()}
# The header value is in the format of:
# {lower_case_header_name: [{"key": "Title_Case_Header_Name": "value": "value"}]}
# And the authorization header value is "Token api_key"
token = token_header[0]["value"].split(" ")[-1]
params["token"] = token
request["querystring"] = urlencode(params)

return request
20 changes: 20 additions & 0 deletions cloudfront-lambda-functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "EC API public access: TLS, CDN, DNS"

Resources:
AuthHeaderToQueryStringFunction:
Type: AWS::Serverless::Function
Properties:
Role: !Sub "arn:aws:iam::${AWS::AccountId}:role/ECApiLambdaExecutionRole"
CodeUri: ./api_endpoints/lambda_edge/auth_header_to_query
Handler: handler.lambda_handler
Runtime: python3.8
AutoPublishAlias: live

Outputs:
AuthHeaderToQueryStringFunctionVersion:
Description: The version ARN of the ID of the AuthHeaderToQueryStringFunction
Value: !Ref AuthHeaderToQueryStringFunction.Version
Export:
Name: "AuthHeaderToQueryString"
9 changes: 9 additions & 0 deletions public-access-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Parameters:
PublicFqdn:
Type: String

AuthHeaderToQueryStringARN:
Type: String

Resources:
APIResponsePolicy:
Type: AWS::CloudFront::ResponseHeadersPolicy
Expand Down Expand Up @@ -47,6 +50,9 @@ Resources:
RemoveHeadersConfig:
Items:
- Header: Vary



CloudFrontDistribution:
Type: 'AWS::CloudFront::Distribution'
Properties:
Expand Down Expand Up @@ -117,6 +123,9 @@ Resources:
- AllowedMethods: [ GET, HEAD, OPTIONS ]
PathPattern: api/*
TargetOriginId: StarletteAPI
LambdaFunctionAssociations:
- EventType: 'viewer-request'
LambdaFunctionARN: !Ref AuthHeaderToQueryStringARN
ForwardedValues:
QueryString: true
Cookies:
Expand Down
17 changes: 17 additions & 0 deletions samconfig.toml.d/ci-production.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ region = "eu-west-2"
[production-public-access]
[production-public-access.deploy]
[production-public-access.deploy.parameters]
s3_bucket = "ec-api-deployment-artifacts-production-6jfsds0gj3f"
template = "public-access-template.yaml"
stack_name = "ECApiPublicAccess-production"
region = "eu-west-2"
Expand All @@ -45,3 +46,19 @@ fail_on_empty_changeset = false
force_upload = true
# Using a "parameter_overrides" setting here would block using CI envvars, as only
# one overrides source is used and this source doesn't pass through shell interpolation.

[production-cloudfront-lambda-functions]
[production-cloudfront-lambda-functions.deploy]
[production-cloudfront-lambda-functions.deploy.parameters]
template = "cloudfront-lambda-functions.yaml"
s3_bucket = "ec-api-cloudfront-functions-production-6jfsds0gj3f"
stack_name = "ECApiCloudFrontFunctions-production"
region = "us-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
tags = "dc-product=\"ec-api\" dc-environment=\"production\""
progressbar = false
fail_on_empty_changeset = false
force_upload = true
# Using a "parameter_overrides" setting here would block using CI envvars, as only
# one overrides source is used and this source doesn't pass through shell interpolation.
17 changes: 17 additions & 0 deletions samconfig.toml.d/ci-staging.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ region = "eu-west-2"
[staging-public-access.deploy]
[staging-public-access.deploy.parameters]
template = "public-access-template.yaml"
s3_bucket = "ec-api-deployment-artifacts-staging-6jfsds0gj3f"
stack_name = "ECApiPublicAccess-staging"
region = "eu-west-2"
confirm_changeset = false
Expand All @@ -45,3 +46,19 @@ fail_on_empty_changeset = false
force_upload = true
# Using a "parameter_overrides" setting here would block using CI envvars, as only
# one overrides source is used and this source doesn't pass through shell interpolation.

[staging-cloudfront-lambda-functions]
[staging-cloudfront-lambda-functions.deploy]
[staging-cloudfront-lambda-functions.deploy.parameters]
template = "cloudfront-lambda-functions.yaml"
s3_bucket = "ec-api-cloudfront-functions-staging-6jfsds0gj3f"
stack_name = "ECApiCloudFrontFunctions-staging"
region = "us-east-1"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
tags = "dc-product=\"ec-api\" dc-environment=\"staging\""
progressbar = false
fail_on_empty_changeset = false
force_upload = true
# Using a "parameter_overrides" setting here would block using CI envvars, as only
# one overrides source is used and this source doesn't pass through shell interpolation.
2 changes: 2 additions & 0 deletions template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ Resources:
# - token
# ReauthorizeEvery: 3600


APIAuthFunction:
Type: AWS::Serverless::Function
Properties:
Role: !Sub "arn:aws:iam::${AWS::AccountId}:role/ECApiLambdaExecutionRole"
CodeUri: ./api_endpoints/api_auth
Handler: handler.lambda_handler
AutoPublishAlias: live
Environment:
Variables:
POSTGRES_HOST: !Ref AppPostgresHost
Expand Down

0 comments on commit 6e02c9d

Please sign in to comment.