UNPKG

remix-utils

Version:

This package contains simple utility functions to use with [React Router](https://reactrouter.com/).

120 lines 4.8 kB
import { sha256 } from "@oslojs/crypto/sha2"; import { decodeBase64 } from "@oslojs/encoding"; import { unstable_createContext, } from "react-router"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/; const USER_PASS_REGEXP = /^([^:]*):(.*)$/; export function unstable_createBasicAuthMiddleware(options) { const userInOptions = "user" in options; const verifyUserInOptions = "verifyUser" in options; if (!(userInOptions || verifyUserInOptions)) { throw new Error('unstable_createBasicAuthMiddleware requires options for "username and password" or "verifyUser"'); } const realm = (options.realm ?? "Secure Area") .replace(/\\/g, "\\\\") .replace(/"/g, '\\"'); const userContext = unstable_createContext(); return [ async function basicAuthMiddleware({ request, context }, next) { let authorization = getAuthorization(request); if (!authorization) throw await unauthorized(request, context); let { username, password } = authorization; if (!username || !password) throw await unauthorized(request, context); if (verifyUserInOptions) { let isValid = await options.verifyUser(username, password, { request, context, }); if (isValid) { context.set(userContext, username); return await next(); } } else { if (Array.isArray(options.user)) { for (let user of options.user) { if (user.username !== username) continue; if (user.password !== password) continue; if (await validateCredentials(user, username, password)) { context.set(userContext, username); return await next(); } } } else { if (await validateCredentials(options.user, username, password)) { context.set(userContext, username); return await next(); } } } throw await unauthorized(request, context); }, function getUser(context) { return context.get(userContext); }, ]; async function getInvalidUserMessage(args) { let invalidUserMessage = options.invalidUserMessage; if (invalidUserMessage === undefined) return "Unauthorized"; if (typeof invalidUserMessage === "string") return invalidUserMessage; if (typeof invalidUserMessage === "function") { return await invalidUserMessage(args); } return invalidUserMessage; } async function validateCredentials(user, username, password) { let [usernameEqual, passwordEqual] = await Promise.all([ timingSafeEqual(user.username, username, options.hashFunction), timingSafeEqual(user.password, password, options.hashFunction), ]); return usernameEqual && passwordEqual; } async function unauthorized(request, context) { let message = await getInvalidUserMessage({ request, context }); return Response.json(message, { status: 401, statusText: "Unauthorized", headers: { "WWW-Authenticate": `Basic realm="${realm}"` }, }); } } async function timingSafeEqual(a, b, hashFunction) { // biome-ignore lint/style/noParameterAssign: This is ok if (!hashFunction) hashFunction = sha256; let [sa, sb] = await Promise.all([ hashFunction(encoder.encode(a.toString())), hashFunction(encoder.encode(b.toString())), ]); if (!sa || !sb) { return false; } return decoder.decode(sa) === decoder.decode(sb) && a === b; } function getAuthorization(request) { let match = CREDENTIALS_REGEXP.exec(request.headers.get("Authorization") || ""); if (!match) return undefined; let userPass = null; // If an invalid string is passed to atob(), it throws a `DOMException`. try { userPass = USER_PASS_REGEXP.exec(decoder.decode(decodeBase64(match[1] ?? ""))); } catch { } // Do nothing if (!userPass) return undefined; let username = userPass[1]; let password = userPass[2]; if (!username || !password) return undefined; return { username, password }; } //# sourceMappingURL=basic-auth.js.map