DEV Community

Cover image for Basic Authentication with Lambda@Edge
Sebastian Bille
Sebastian Bille

Posted on • Updated on

Basic Authentication with Lambda@Edge

πŸ–Ό Background

Recently I was asked to "secure" (as in; make it not super public) a static website, hosted in S3, by adding Basic Authentication as a quick and dirty solution to just require a simple password in order to access the site. This article will explain how that can be achieved with the help of Cloudfront and Lambda@Edge. Please note that it's a horrible idea to use this for anything that's actually sensitive, it's just a very quick and simple way to add a password requirement for a static website. It's also a fun project to get your hands dirty with Lambda@Edge! I'm going to assume that you already have a website hosted in S3 which is fronted by a Cloudfront distribution - if you don't, there's plenty of guides on how to set that up out there on the interwebz.

🀫 Just get to it dude

Alright, alright, let's get started. The idea here is that we can use Lambda@Edge to do our actual authentication by intercepting requests by hooking into the Cloudfront request lifecycle.

Let's start by creating our serverless app by initializing a new project in an empty folder with npm init -y. Now let's install what we need to deploy our service:

npm install serverless serverless-lambda-edge-pre-existing-cloudfront --save-dev
Enter fullscreen mode Exit fullscreen mode

Other than having a super catchy name, the serverless-lambda-edge-pre-existing-cloudfront plugin allows us to hook up a Lambda@Edge function to a pre-existing Cloudfront distribution.

Next, let's create our Lambda function:

// basic-auth.js
const handler = async (event) => {
  const { request } = event.Records[0].cf;
  const headers = request.headers;

  const username = 'username';
  const password = 'password';

  const base64Credentials = Buffer.from(`${username}:${password}`).toString('base64');
  const authString = `Basic ${base64Credentials}`;

  // If authorization header isn't present or doesn't match expected authString, deny the request
  if (
    typeof headers.authorization == 'undefined' ||
    headers.authorization[0].value !== authString
  ) {
    return {
      body: 'Unauthorized',
      headers: {
        'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Basic' }]
      },
      status: '401',
      statusDescription: 'Unauthorized',
    };
  }

  // Continue request processing
  return request;
};

module.exports.handler = handler;
Enter fullscreen mode Exit fullscreen mode

It's obviously never a good idea to hardcode the username & password in the code and you can use for example a DynamoDB table to fetch these at runtime instead. Do keep in mind however that Lambda@Edge does not support environment variables. In fact, Lambda@Edge does have quite a lot of quirks and unexpected limitations so it might be a good idea to have an extra look at limitations documentation if you change anything and run into problems.

Now, let's describe our beautiful serverless service in a serverless.yml a little something like this:

service:
  name: basic-auth-demo

plugins:
  - serverless-lambda-edge-pre-existing-cloudfront

provider:
  name: aws
  # Cloudfront only supports Lambda@Edge functions defined 
  # in us-east-1
  region: 'us-east-1'
  runtime: nodejs12.x
  versionFunctions: true
  memorySize: 128
  role: role
  timeout: 5

functions:
  basic-auth:
    handler: basic-auth.handler
    events:
      - preExistingCloudFront:
          distributionId: ${env:CLOUDFRONT_DISTRIBUTION_ID}
          pathPattern: '*'
          eventType: viewer-request
          includeBody: false

resources:
  Resources:
    role:
      Type: AWS::IAM::Role
      Properties:
        RoleName: role
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
                  - edgelambda.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaRole
Enter fullscreen mode Exit fullscreen mode

Once we deploy this service, the Lambda function we just created will be attached to the Cloudfront distribution in front of the static website. Do note that you need to set the environment variable CLOUDFRONT_DISTRIBUTION_ID to the id of your distribution.

Assuming you have valid AWS credentials in your [default] profile of ~/.aws/credentials you can now deploy this service:

export CLOUDFRONT_DISTRIBUTION_ID=abc123 
npx serverless deploy 
Enter fullscreen mode Exit fullscreen mode

If you now go to access your website, you should be greeted with a very unpleasant dialog asking you to immediately explain who you are πŸŽ‰

angry sign in dialog

By now you might be asking:

But Mr. Elk, can't someone just access my website by going straight to the S3 resource, bypassing Cloudfront?

Excellent question anonymous internet person #12339 - no. Not if you make sure to restrict access to the S3 files using an Origin Access Identity (which you should probably have anyway).

If you enjoyed this guide and want to see more, follow me on Twitter at @TastefulElk where I frequently write about serverless tech, AWS and developer productivity!

Happy hacking! πŸš€

Top comments (5)

Collapse
 
hshar7 profile image
Hayder Sharhan

With Cloudfront's new functions feature:

function handler (event) {
  var request = event.request;
  var headers = request.headers;
  var authString = "Basic BASE64_OF_USERNAME:PASSWORD";

  // If authorization header isn't present or doesn't match expected authString, deny the request
  if (
    typeof headers['authorization'] == 'undefined' ||
    headers['authorization'].value !== authString
  ) {
      return {
        statusCode: 401,
        statusDescription: 'Unauthorized',
        headers: {
            'www-authenticate': { value: 'Basic' }
        },
      };
  }

  return request;
};
Enter fullscreen mode Exit fullscreen mode

And then associate the function with the distribution

EEEEZZZ

Collapse
 
thedubcoder profile image
w

Please note that it's a horrible idea to use this for anything that's actually sensitive

Can you explain why?

Collapse
 
wulfmann profile image
Joseph Snell

Biggest reason I see is that you'd have to hardcode the username/password in code which means it would likely end up in source control. Not to mention this limits you to a single, static username/password combo which is in and of itself insecure.

Collapse
 
leenattress profile image
Lee Nattress

The check occurs in a lambda. It would be trivial to query cognito, a dynamodb or any other type of storage here. You should never just use code from the web, this is an example of the setup, and may I say thankyou to the original author, it helped me a great deal.

Collapse
 
timo_ernst profile image
Timo Ernst

Thanks for sharing. Can you extend this so the password would be stored in SecretsManager instead of being saved in plain text within the lambda function?