HTTP Basic Authentication
Shows how to restrict access using the HTTP Basic schema.
/*** Shows how to restrict access using the HTTP Basic schema.* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication* @see https://tools.ietf.org/html/rfc7617** A user-id containing a colon (":") character is invalid, as the* first colon in a user-pass string separates user and password.*/export default {async fetch(request) {const BASIC_USER = "admin";const BASIC_PASS = "admin";/*** Throws exception on verification failure.* @param {string} user* @param {string} pass* @throws {UnauthorizedException}*/async function verifyCredentials(user, pass) {if (BASIC_USER !== user) {throw new UnauthorizedException("Invalid credentials.");}if (BASIC_PASS !== pass) {throw new UnauthorizedException("Invalid credentials.");}}/*** Parse HTTP Basic Authorization value.* @param {Request} request* @throws {BadRequestException}* @returns {{ user: string, pass: string }}*/async function basicAuthentication(request) {const Authorization = request.headers.get("Authorization");const [scheme, encoded] = Authorization.split(" ");// The Authorization header must start with Basic, followed by a space.if (!encoded || scheme !== "Basic") {throw new BadRequestException("Malformed authorization header.");}// Decodes the base64 value and performs unicode normalization.// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalizeconst buffer = Uint8Array.from(atob(encoded), (character) =>character.charCodeAt(0));const decoded = new TextDecoder().decode(buffer).normalize();// The username & password are split by the first colon.//=> example: "username:password"const index = decoded.indexOf(":");// The user & password are split by the first colon and MUST NOT contain control characters.// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {throw new BadRequestException("Invalid authorization value.");}return {user: decoded.substring(0, index),pass: decoded.substring(index + 1),};}async function UnauthorizedException(reason) {this.status = 401;this.statusText = "Unauthorized";this.reason = reason;}async function BadRequestException(reason) {this.status = 400;this.statusText = "Bad Request";this.reason = reason;}const { protocol, pathname } = new URL(request.url);// In the case of a Basic authentication, the exchange MUST happen over an HTTPS (TLS) connection to be secure.if ("https:" !== protocol ||"https" !== request.headers.get("x-forwarded-proto")) {throw new BadRequestException("Please use a HTTPS connection.");}switch (pathname) {case "/":return new Response("Anyone can access the homepage.");case "/logout":// Invalidate the "Authorization" header by returning a HTTP 401.// We do not send a "WWW-Authenticate" header, as this would trigger// a popup in the browser, immediately asking for credentials again.return new Response("Logged out.", { status: 401 });case "/admin": {// The "Authorization" header is sent when authenticated.if (request.headers.has("Authorization")) {// Throws exception when authorization fails.const { user, pass } = basicAuthentication(request);verifyCredentials(user, pass);// Only returns this response when no exception is thrown.return new Response("You have private access.", {status: 200,headers: {"Cache-Control": "no-store",},});}// Not authenticated.return new Response("You need to login.", {status: 401,headers: {// Prompts the user for credentials."WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"',},});}case "/favicon.ico":case "/robots.txt":return new Response(null, { status: 204 });}return new Response("Not Found.", { status: 404 });},};
const handler: ExportedHandler = {async fetch(request: Request) {const BASIC_USER = "admin";const BASIC_PASS = "admin";/*** Throws exception on verification failure.* @param {string} user* @param {string} pass* @throws {UnauthorizedException}*/async function verifyCredentials(user, pass) {if (BASIC_USER !== user) {throw new UnauthorizedException("Invalid credentials.");}if (BASIC_PASS !== pass) {throw new UnauthorizedException("Invalid credentials.");}}/*** Parse HTTP Basic Authorization value.* @param {Request} request* @throws {BadRequestException}* @returns {{ user: string, pass: string }}*/async function basicAuthentication(request) {const Authorization = request.headers.get("Authorization");const [scheme, encoded] = Authorization.split(" ");// The Authorization header must start with Basic, followed by a space.if (!encoded || scheme !== "Basic") {throw new BadRequestException("Malformed authorization header.");}// Decodes the base64 value and performs unicode normalization.// @see https://datatracker.ietf.org/doc/html/rfc7613#section-3.3.2 (and #section-4.2.2)// @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalizeconst buffer = Uint8Array.from(atob(encoded), (character) =>character.charCodeAt(0));const decoded = new TextDecoder().decode(buffer).normalize();// The username & password are split by the first colon.//=> example: "username:password"const index = decoded.indexOf(":");// The user & password are split by the first colon and MUST NOT contain control characters.// @see https://tools.ietf.org/html/rfc5234#appendix-B.1 (=> "CTL = %x00-1F / %x7F")if (index === -1 || /[\0-\x1F\x7F]/.test(decoded)) {throw new BadRequestException("Invalid authorization value.");}return {user: decoded.substring(0, index),pass: decoded.substring(index + 1),};}async function UnauthorizedException(reason) {this.status = 401;this.statusText = "Unauthorized";this.reason = reason;}async function BadRequestException(reason) {this.status = 400;this.statusText = "Bad Request";this.reason = reason;}const { protocol, pathname } = new URL(request.url);// In the case of a Basic authentication, the exchange MUST happen over an HTTPS (TLS) connection to be secure.if ("https:" !== protocol ||"https" !== request.headers.get("x-forwarded-proto")) {throw new BadRequestException("Please use a HTTPS connection.");}switch (pathname) {case "/":return new Response("Anyone can access the homepage.");case "/logout":// Invalidate the "Authorization" header by returning a HTTP 401.// We do not send a "WWW-Authenticate" header, as this would trigger// a popup in the browser, immediately asking for credentials again.return new Response("Logged out.", { status: 401 });case "/admin": {// The "Authorization" header is sent when authenticated.if (request.headers.has("Authorization")) {// Throws exception when authorization fails.const { user, pass } = basicAuthentication(request);verifyCredentials(user, pass);// Only returns this response when no exception is thrown.return new Response("You have private access.", {status: 200,headers: {"Cache-Control": "no-store",},});}// Not authenticated.return new Response("You need to login.", {status: 401,headers: {// Prompts the user for credentials."WWW-Authenticate": 'Basic realm="my scope", charset="UTF-8"',},});}case "/favicon.ico":case "/robots.txt":return new Response(null, { status: 204 });}return new Response("Not Found.", { status: 404 });},};export default handler;