Authentication (Cloudflare)
a sign-in in front of a wiki published on Cloudflare
This page is a draft. The owner has not approved what it says this part of the system is for, so read it as a proposal rather than as the wiki.
A wiki published on Cloudflare Pages can ask for a sign-in before it shows any page, in one of two ways: Cloudflare Access, which emails each allowed reader a sign-in code, or a Pages Function that asks the browser for a shared user name and password.[1][2] Cloudflare recommends Access for a production sign-in, and a password prompt is safe only over HTTPS, because it sends the password unencrypted.[3] Publishing the wiki is described on Deployment (Cloudflare).
wiki publish writes a markdown copy beside each page and an llms.txt listing them, and each holds the
wiki's text, so a sign-in guards those files as well as the pages.[4]
Cloudflare Access
Access lets in only the email addresses its policy allows, and each reader signs in with a code sent to
that address, with no identity provider to connect.[1][5] The code expires ten minutes after the
reader asks for it.[1] A policy's Allow action admits the readers its rules match: named addresses
under Emails, or a whole domain under Emails ending in.[5] Access protects a whole address, so the
markdown copies and llms.txt sit behind it with the pages.[6][4]
Each address the wiki is served from is protected on its own.[7][8]
| Address | Setting |
|---|---|
| Preview deployments | the project's Settings, then Enable access policy[7] |
PROJECT.pages.dev |
edit the policy that Enable access policy creates, delete the * from its Subdomain field and save, then enable the access policy again[8] |
| Custom domain | under Zero Trust, Access controls, then Applications: Create new application, Self-hosted and private, Add public hostname, and the domain[9] |
A custom domain without its own Access policy shows a sign-in that does not work.[9] The Zero Trust Free plan asks for a payment method when the account is set up, and does not charge it.[10]
Password prompt
A Pages Function at functions/_middleware.js runs in front of every request, static files included, and
passes an accepted request on to the page.[2] The functions folder sits at the root of the
repository Cloudflare builds, not inside _site.[11] The user name and password are Pages secrets,
added with Encrypt selected under the project's Settings, then Variables and Secrets, before the
deployment that reads them.[12]
The middleware below adapts Cloudflare's Basic Authentication example to Pages: it reads WIKI_USER and
WIKI_PASSWORD from those secrets, compares both in constant time, and answers a missing or wrong password
with the 401 that makes the browser ask again.[3][12]
// functions/_middleware.js
const encoder = new TextEncoder();
// Compares in constant time, so the time taken reveals nothing about the password.
function timingSafeEqual(a, b) {
const aBytes = encoder.encode(a);
const bBytes = encoder.encode(b);
if (aBytes.byteLength !== bBytes.byteLength) {
return !crypto.subtle.timingSafeEqual(aBytes, aBytes);
}
return crypto.subtle.timingSafeEqual(aBytes, bBytes);
}
function signIn() {
return new Response("Sign in to read the wiki.", {
status: 401,
headers: { "WWW-Authenticate": 'Basic realm="wiki", charset="UTF-8"' },
});
}
export async function onRequest(context) {
const { WIKI_USER, WIKI_PASSWORD } = context.env;
if (!WIKI_USER || !WIKI_PASSWORD) {
return new Response("WIKI_USER and WIKI_PASSWORD are not set.", { status: 500 });
}
const [scheme, encoded] = (context.request.headers.get("Authorization") || "").split(" ");
if (scheme !== "Basic" || !encoded) {
return signIn();
}
let credentials;
try {
credentials = atob(encoded);
} catch {
return signIn();
}
const colon = credentials.indexOf(":");
const user = credentials.substring(0, colon);
const password = credentials.substring(colon + 1);
if (colon < 0 || !timingSafeEqual(WIKI_USER, user) || !timingSafeEqual(WIKI_PASSWORD, password)) {
return signIn();
}
return context.next();
}