Last active
August 20, 2022 23:09
-
-
Save dynamis/4ebac2c9b1519c223daf27332530e0ea to your computer and use it in GitHub Desktop.
basic auth worker for cloudflare pages
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 = 'username'; | |
const BASIC_PASS = 'password'; | |
/** | |
* Receives a HTTP request and replies with a response. | |
* @param {Request} request | |
* @returns {Promise<Response>} | |
*/ | |
async function handleRequest(request, env) { | |
// return env.ASSETS.fetch(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.'); | |
} | |
if (request.headers.has('Authorization')) { | |
// Throws exception when authorization fails. | |
const { user, pass } = basicAuthentication(request); | |
verifyCredentials(user, pass); | |
// Only returns static assets file when no exception is thrown. | |
return env.ASSETS.fetch(request); | |
} | |
// 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"', | |
}, | |
}); | |
} | |
/** | |
* 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 username.'); | |
} | |
if (BASIC_PASS !== pass) { | |
throw new UnauthorizedException('Invalid password.'); | |
} | |
} | |
/** | |
* 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; | |
} | |
export default { | |
async fetch(request, env) { | |
return handleRequest(request, env).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, | |
} | |
}) | |
}) | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment