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. */
const BASIC_USER = 'admin';
const BASIC_PASS = 'admin';
/** * Receives a HTTP request and replies with a response. * @param {Request} request * @returns {Promise<Response>} */
async function handleRequest(request) { 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 });
}
/** * Throws exception on verification failure. * @param {string} user * @param {string} pass * @throws {UnauthorizedException} */
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 }} */
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), };
}
function UnauthorizedException(reason) { this.status = 401; this.statusText = 'Unauthorized'; this.reason = reason;
}
function BadRequestException(reason) { this.status = 400; this.statusText = 'Bad Request'; this.reason = reason;
}
addEventListener('fetch', event => { event.respondWith( handleRequest(event.request).catch(err => { const message = err.reason || err.stack || 'Unknown Error';
return new Response(message, { status: err.status || 500, statusText: err.statusText || null, headers: { 'Content-Type': 'text/plain;charset=UTF-8', // Disables caching by default. 'Cache-Control': 'no-store', // Returns the "Content-Length" header for HTTP HEAD requests. 'Content-Length': message.length, }, }); }) );
});