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
- https://developers.cloudflare.com/pages/platform/functions/
- https://phiilu.medium.com/password-protect-your-vercel-site-with-cloudflare-workers-a0070357a005
- https://developers.cloudflare.com/workers/examples/basic-auth/
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(); }