HTTP Basic Auth with „Full Stack“ CloudFlare Pages

In November 2021 CloudFlare announced that they added Workers support to CloudFlare Pages. With this we can easily integrate Workers to secure a site deployed with CloudFlare Pages with Basic Auth!

Keep in mind that the limit for Workers is 100,000 requests per month on the free tier. It’s enough for my use case.

Last Updated: 28th January 2023

Preparing the site

Just copy this Worker script (or see below) into the directory functions in the ROOT of your project (NOT the root of the build output!) and name it as [[path]].js. The script allows unauthenticated requests to the favicon.ico and robots.txt in the root, but you can easily change that. It will automatically protect all other routes with a username and password from the environment variables USER and PASS.

Setting credentials

Navigate to your page in the CloudFlare Pages dashboard, click „Settings“ and then „Environment variables“. Add two variables to both preview and production with the names „USER“ and „PASS“ and the username plus the (strong!) password you want.

Setting failure mode for request limit

Added on 28th January 2023

If you reach the request limit of 100,000 requests per day (on a free plan) the basic auth will be bypassed! To change this, go to your Cloudflare Pages dashboard, edit your site and under „Settings“ -> „Functions“, change „Request limit failure mode“ to „Fail closed (block)„.

Testing the worker locally

Install the beta version of Wrangler:

npm install -g wrangler@beta

Run it inside your project directory and bind the variables:

npx wrangler pages dev PATH_TO_YOUR_BUILD_OUTPUT --binding USER="myuser" PASS="mypass"

Read more

The workers script

// Thanks to https://phiilu.medium.com/password-protect-your-vercel-site-with-cloudflare-workers-a0070357a005
// See: https://brawl.vivaldi.net/?p=208

const CREDENTIALS_REGEXP = /^ *[Bb][Aa][Ss][Ii][Cc] +([\w+./~-]+=*) *$/;

const USER_PASS_REGEXP = /^([^:]*):(.*)$/;

class Credentials {
  constructor(name, pass) {
    this.name = name;
    this.pass = pass;
  }
}

const parseAuthHeader = (string) => {
  if (typeof string !== 'string') {
    return null;
  }

  // parse header
  const match = CREDENTIALS_REGEXP.exec(string);

  if (!match) {
    return null;
  }

  // decode user pass
  const userPass = USER_PASS_REGEXP.exec(atob(match[1]));

  if (!userPass) {
    return null;
  }

  // return credentials object
  return new Credentials(userPass[1], userPass[2]);
};

const unauthorizedResponse = (body) =>
  new Response(body, {
    status: 401,
    headers: {
      'WWW-Authenticate': 'Basic realm="docs"',
    },
  });

export async function onRequest({ env, next, request }) {
  const { pathname } = new URL(request.url);

  if (pathname === '/favicon.ico' || pathname === '/robots.txt') {
    return next();
  }

  const credentials = parseAuthHeader(request.headers.get('Authorization'));
  if (
    !credentials ||
    credentials.name !== env.USER ||
    credentials.pass !== env.PASS
  ) {
    return unauthorizedResponse('Unauthorized');
  }
  return next();
}