awslabs / aws-lambda-web-adapter

Run web applications on AWS Lambda
Apache License 2.0
1.92k stars 115 forks source link

Rename headers #477

Closed tmokmss closed 3 months ago

tmokmss commented 3 months ago

Feature request: Add option to let LWA to replace HTTP header names before proxying the request to the server.

Use case: When we use Lambda function URL with CloudFront, we often use AWS_IAM auth type for security, and sign each request in Lambda@Edge (ref). This is still a viable option because OAC does not suppot PUT or POST requests (doc). When we sign a request with sigv4, Authorization header is used to pass a signature, which overrides the original Authorization header even if the backend requires it for authentication. This results in the backend not working as expected when used with Lambda fURL.

So this issue proposes LWA to allow to rename headers before proxying the request. Lambda@Edge copies the original Authorization header to other name, and LWA move it back after Lambda function URL used the sigV4 Authorizaton header.

See the below diagram.

Current: image

If LWA supports renaming headers: image

I also tried passing the signature via querystring, but that was not ideal, because the backend apps are often so "aware" of querystrings that they behave unexpectedly. (e.g. Next.js router adds all the querystrings to the response header as Next-Router-State-Tree, which exposes all the sigv4 data to the client.)

bnusunny commented 3 months ago

Good suggestion! We could configure this behaviour with an env var AWS_LWA_AUTHERIZATION_SOURCE, default to None. When this env var is configured, LWA will take the value from that header to overwrite the Authorization header.

gsleite commented 3 months ago

You can execute POST using OAC. Create a SHA-256 hash digest and put it in "x-amz-content-sha256" header.

This is an example of a javascript logic to create the hash:

const hashPayload = async (payload) => { const encoder = new TextEncoder().encode(payload); const hash = await crypto.subtle.digest('SHA-256', encoder); const hashArray = Array.from(new Uint8Array(hash)); return hashArray .map((bytes) => bytes.toString(16).padStart(2, '0')) .join('');

}

header["x-amz-content-sha256"] = await hashPayload(payload);

The OAC will use the Authorizer header to send the signed request to lambda, you do not need to create any other logic for sigv4 request. If you would like to inject another token in your application, you can do it in lambda@edge authorizer:

const payload = await verifier.verify(headers["x-access-token"][0].value); console.log("Token is valid. Payload:", payload); //Add decoded access token to header request.headers["x-access-token"][0].value = JSON.stringify(payload); callback(null, request)

You can find the complete solution in this sample:

https://github.com/aws-samples/serverless-genai-assistant/tree/main/examples/sample_web_application

tmokmss commented 3 months ago

Thanks @gsleite. So the OAC still overwrites Authorization header and hence the original problem persists (correct me if I misunderstand something!). It is sometimes difficult for a backend app to use a header name other than Authorization.

OAC provides no-override option but it seems it just not work when the browser provides application-specific Authorization header (not sigv4).

Don't override the viewer (client) Authorization header This setting is named Do not override authorization header in the console, or no-override in the API, CLI, and AWS CloudFormation. Use this setting when you want CloudFront to sign origin requests only when the corresponding viewer request does not include an Authorization header. With this setting, CloudFront passes on the Authorization header from the viewer request when one is present, but signs the origin request (adding its own Authorization header) when the viewer request doesn't include an Authorization header. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html

But I guess we should use OAC to simplify L@E code. I'll rewrite my current code, thanks.

@bnusunny Thanks! I think your suggestion will work for my use case. One thing to consider is whether we should generalize the solution (e.g. pass a map of old/new header names), or limit this feature only for Authorization header (as you suggested).

gsleite commented 3 months ago

@tmokmss, yes, your original problem will persist. If you try to override the Authorization header the Lambda will return 403 as you mentioned.

What i do in my use case is add a custom header ["x-access-token"], (avoiding querystring), and use the lambda@edge to validate the token. If the token is valid i pass it decoded to the backend using the same custom header, this is the same behaviour of Authorization header in API GW with Lambda Proxy.

This solution may save you some time.

tmokmss commented 3 months ago

I see. So you are using L@E as a Lambda authorizer. In my usecase, though, the user related data is in Aurora, and only the backend can consume Authorization header, which is often the case with a fullstack webapp framework like Next.js.

bnusunny commented 3 months ago

@tmokmss I want to keep LWA features at minumum. Let's do this specific thing for Authorization header. If people need to add/remove headers in a flexible way, they always could include Nginx in the package.

tmokmss commented 3 months ago

@bnusunny That makes sense. I'll submit a PR if you are not working on this :)

bnusunny commented 3 months ago

Go ahead! Thanks!