remix-utils
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/).
120 lines • 4.8 kB
JavaScript
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