Authorizing requests with Lambda@Edge
Tags: aws, security, serverless
Published: 2022-11-03 07:50:01

We can use Lambda@Edge functions to authorize requests that come to our CloudFront distribution. This way, the request won't proceed to the origin if it doesn't contain the required headers.

## 1. A scenario

Assume that our application wants to receive near-real-time notifications from a 3rd party provider. We are setting up a **webhook** where the partner can send the events.

[Lambda function URLs](https://dev.to/aws-builders/controlling-access-to-lambda-function-urls-idd) are a great choice for this purpose. They are easy to set up, and an <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a> - although it's a proper solution - would probably be overkill here.

One drawback of Lambda function URLs is that they don't allow custom domains as of writing this. A workaround to this issue is to create a <a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html" class="link" target="_blank" rel="noopener noreferrer">CloudFront</a> distribution where the origin is the function URL. We can then add the distribution as a target for the <a href="https://aws.amazon.com/route53/" class="link" target="_blank" rel="noopener noreferrer">Route 53</a> record.

But we somehow should authorize the webhook requests and let only those reach the origin which contains the required token in the relevant header.

Let's see a solution to this scenario.

## 2. Authorization with Lambda@Edge

We can use a <a href="https://aws.amazon.com/lambda/edge/" class="link" target="_blank" rel="noopener noreferrer">Lambda@Edge</a> function to perform the authorization.

### 2.1. What this post doesn't contain

This article's focus is on permissions and some other minor details. It won't explain how to

<ul>
  <li>create a CloudFront distribution</li>
  <li>attach Lambda@Edge functions to the distribution</li>
  <li>create Lambda functions with a URL</li>
  <li>add the function URL to the distribution as the origin.</li>
</ul>

I'll have some links at the bottom of the page that describe these operations.

### 2.2. Some code

Let's start with the Lambda@Edge function. Since it's just a regular <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> function that AWS distributes and deploys to the edge locations, we can have a - more or less - standard Node.js code:

```js
const { SSM } = require('aws-sdk');

const ssm = new SSM({region: 'us-east-1'});

exports.handler = async (event) => {
  const request = event.Records[0].cf.request;

  const apiKeyHeader = request.headers['x-api-key'];
  if (!apiKeyHeader || apiKeyHeader.length === 0) {
      throw new Error('Missing token');
  }

  let apiKey;
  try {
    const ssmResponse = await ssm.getParameter({
      Name: '/my/encrypted/secret',
      WithDecryption: true,
    }).promise();
    apiKey = ssmResponse.Parameter.Value;
  } catch (error) {
    throw error;
  }

  const headerValue = apiKeyHeader[0].value;
  if (headerValue !== apiKey) {
    throw new Error('Invalid token');
  }

  return request;
};
```

We store the secret header value in <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" class="link" target="_blank" rel="noopener noreferrer">Parameter Store</a>. The Lambda@Edge function will get it from there and then compares it to the request header. If they match, the function will return the CloudFront request object, and it can then proceed to the origin. Otherwise, we'll throw an error if the required header is missing or the secrets don't match.

That's it, and the post could end here. But - as with CloudFront in general - some small details can make the cloud developer's life more contentful.

## 3. Permissions

Lambda@Edge will use the Lambda execution role we create for the function.

### 3.1. Assume role

We should make the role assumable for Lambda@Edge. The role's **trust policy** should look like this:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "lambda.amazonaws.com",
          "edgelambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

We must add `edgelambda.amazonaws.com` to the `Principal` element so Lambda@Edge can assume the role.

### 3.2. Service permissions

In this example, the Lambda@Edge function calls Parameter Store, so we should add `ssm:GetParameter` and - if the secret is in a `SecureString` format - `kms:Decrypt` permissions to the role.

These are not Lambda@Edge-specific permission. A regular Lambda function performing the same logic should also have these permissions.

### 3.3. Logging into a different region

We can think that we are good to go. We might, but there is a chance that we will receive the following error if we invoke the CloudFront distribution (or the custom domain) URL:

```
503 ERROR
The Lambda function associated with the CloudFront distribution
is invalid or doesn't have the required permissions. We can't
connect to the server for this app or website at this time. There
might be too much traffic or a configuration error. Try again later,
or contact the app or website owner.
```

The error message refers to some missing permissions.

CloudFront deploys the Lambda function to at least some edge locations different from the region where we created it. When we invoke the CloudFront URL, the Lambda@Edge closest to our geographic location will run.

In this example, I created the function in `us-east-1`, but the edge location closest to me is `eu-central-1`. It means the Lambda function in `eu-central-1` will run and perform the authorization logic!

If it runs in `eu-central-1`, it will log to this region too. The problem is that it currently doesn't have permission to do so.

Originally the <a href="https://aws.amazon.com/cloudwatch/" class="link" target="_blank" rel="noopener noreferrer">CloudWatch</a> logs permissions look like this:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "logs:CreateLogGroup",
      "Resource": "arn:aws:logs:us-east-1:123456789012:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": [
        "arn:aws:logs:us-east-1:123456789012:log-group:/aws/lambda/FUNCTION_NAME:*"
      ]
    }
  ]
}
```

Lambda automatically attaches this policy to the execution role when we create a function in the Console (or using some frameworks). The `Resource` elements all point to the log group in `us-east-1`.

Let's rectify the error. We can replace `us-east-1` with a `*` character in the statement:

```json
{
  "Effect": "Allow",
  "Action": "logs:CreateLogGroup",
  "Resource": "arn:aws:logs:*:123456789012:*"
}
```

Every Lambda@Edge function can now create its log groups in the corresponding region.

We should also change the `Resource` for the `logs:PutLogEvents` action. It is because the log group name will be `/aws/lambda/us-east-1.FUNCTION_NAME` in the edge location region.

```json
{
  "Effect": "Allow",
  "Action": [
    "logs:CreateLogStream",
    "logs:PutLogEvents"
  ],
  "Resource": [
    "arn:aws:logs:*:123456789012:log-group:/aws/lambda/FUNCTION_NAME:*",
    "arn:aws:logs:*:123456789012:log-group:/aws/lambda/us-east-1.FUNCTION_NAME:*"
  ]
}
```

CloudFront won't respond with an error if we don't add the edge location version of the log group to the `Resource` element. It will simply not log anything.

## 4. Considerations

Now that we have granted the required permissions let's look at a few potential and actual drawbacks.

### 4.1. Limited package size

Lambda@Edge supports a maximum of 1 MB zipped package size. So we can't really `npm install` anything because we will probably exceed this limit.

Luckily, Lambda supports AWS SDK out of the box. All we have to do is `require` it at the top of the `index.js` handler as if it was an installed dependency.

Lambda comes with <a href="https://aws.amazon.com/sdk-for-javascript/" class="link" target="_blank" rel="noopener noreferrer">SDK</a> v2 out of the box as of writing this. It doesn't support the modular v3 yet, but I don't think of it as a disadvantage for this example because we don't have to worry about the package size here.

Unfortunately, we can't stretch far with other dependencies, so we are forced to use language-native solutions.

### 4.2. No layers

Unfortunately, we can't use the new <a href="https://aws.amazon.com/about-aws/whats-new/2022/10/aws-parameters-secrets-lambda-extension/" class="link" target="_blank" rel="noopener noreferrer">Parameter Store extension</a> because Lambda@Edge doesn't support layers.

It's not a problem in this example because we can use the SDK that Lambda supports out of the box.

### 4.3. Origin request policy

If we invoke the CloudFront URL, the Lambda@Edge authorizer will run, but the logic will throw an exception because it won't receive the header with the secret API key in the request.

We should change the **Origin request policy** in the **Behavior** section to make CloudFront forward all headers to Lambda@Edge.

![Choosing the right origin request policy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/am6x6k8lkpt70zisjlog.png)

If we select `AllViewer` here, the request payload will contain the secret header.

## 5. Conclusion

We can use the Lambda@Edge functions to intercept the requests in CloudFront and perform authorization.

Lambda@Edge uses the same execution role as the corresponding regular Lambda function. We must ensure that we add all regions to the log permissions and forward all headers to the authorizer function.


## 6. Further reading

<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" class="link" target="_blank" rel="noopener noreferrer">Lambda function URLs</a> - Start here for function URLs

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-creating-console.html" class="link" target="_blank" rel="noopener noreferrer">Creating a distribution</a> - How to create a CloudFront distribution

<a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-edge.html" class="link" target="_blank" rel="noopener noreferrer">Using AWS Lambda with CloudFront Lambda@Edge </a> - Add a Lambda function to the distribution

<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html#concept_lambda_function_url" class="link" target="_blank" rel="noopener noreferrer">Using a Lambda function URL</a> - Lambda function URL as CloudFront origin