DEV Community 👩‍💻👨‍💻

Arpad Toth for AWS Community Builders

Posted on • Originally published at arpadt.com

Authorizing requests with Lambda@Edge

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 are a great choice for this purpose. They are easy to set up, and an API Gateway - 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 CloudFront distribution where the origin is the function URL. We can then add the distribution as a target for the Route 53 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 Lambda@Edge 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

  • create a CloudFront distribution
  • attach Lambda@Edge functions to the distribution
  • create Lambda functions with a URL
  • add the function URL to the distribution as the origin.

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 Lambda function that AWS distributes and deploys to the edge locations, we can have a - more or less - standard Node.js code:

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;
};
Enter fullscreen mode Exit fullscreen mode

We store the secret header value in Parameter Store. 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:

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

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.
Enter fullscreen mode Exit fullscreen mode

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 CloudWatch logs permissions look like this:

{
  "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:*"
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "Effect": "Allow",
  "Action": "logs:CreateLogGroup",
  "Resource": "arn:aws:logs:*:123456789012:*"
}
Enter fullscreen mode Exit fullscreen mode

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.

{
  "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:*"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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 SDK 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 Parameter Store extension 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

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

Lambda function URLs - Start here for function URLs

Creating a distribution - How to create a CloudFront distribution

Using AWS Lambda with CloudFront Lambda@Edge - Add a Lambda function to the distribution

Using a Lambda function URL - Lambda function URL as CloudFront origin

Top comments (0)

18 Useful Github Repositories Every Developer Should Bookmark

18 Useful GitHub repositories every developer should bookmark: everything from learning resources and roadmaps to best practices, system designs, and tools.