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/normalize      const 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/normalize      const 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;