import { NIFRA_ASSURANCE, withRouteAssurance } from "@nifrajs/core/assurance" import { defineIdentityPlugin, type IdentityPlugin } from "@nifrajs/core/server" import { decodeBase64, jsonError, type MaybePromise, quotedHeaderValue, sha256, timingSafeEqualBytes, } from "./_utils.ts" export type BasicAuthPlugin
= IdentityPlugin & { principal(request: Request): P | null requirePrincipal(request: Request): P } export interface BasicAuthStaticOptions
{ readonly username: string readonly password: string readonly principal?: P readonly realm?: string readonly optional?: boolean } export interface BasicAuthVerifyOptions
{ readonly verify: (username: string, password: string) => MaybePromise
readonly realm?: string readonly optional?: boolean } const UTF8 = new TextDecoder("utf-8", { fatal: true }) function challenge(realm: string): string { return `Basic realm="${quotedHeaderValue(realm)}", charset="UTF-8"` } function reject(realm: string): Response { return jsonError(401, "unauthorized", { "www-authenticate": challenge(realm) }) } function credentials(request: Request): { username: string; password: string } | null { const header = request.headers.get("authorization") if (header?.startsWith("Basic ") !== true) return null const raw = decodeBase64(header.slice(6).trim()) if (raw === null) return null let decoded: string try { decoded = UTF8.decode(raw) } catch { return null } const colon = decoded.indexOf(":") if (colon < 0) return null return { username: decoded.slice(0, colon), password: decoded.slice(colon + 1) } } async function staticVerifier
( username: string, password: string, principal: P, ): Promise<(u: string, p: string) => Promise
> {
const [expectedUser, expectedPass] = await Promise.all([sha256(username), sha256(password)])
return async (u, p) => {
const [gotUser, gotPass] = await Promise.all([sha256(u), sha256(p)])
const userOk = timingSafeEqualBytes(gotUser, expectedUser)
const passOk = timingSafeEqualBytes(gotPass, expectedPass)
return userOk && passOk ? principal : null
}
}
/**
* HTTP Basic authentication. Prefer short-lived Basic Auth for internal tools and staging gates, not
* public user login. Static credentials are compared in constant time after SHA-256 hashing; the
* callback form is available for external stores.
*/
export function basicAuth(options: BasicAuthStaticOptions): BasicAuthPlugin (options: BasicAuthStaticOptions ): BasicAuthPlugin
export function basicAuth (options: BasicAuthVerifyOptions ): BasicAuthPlugin
export function basicAuth (
options: BasicAuthStaticOptions | BasicAuthVerifyOptions ,
): BasicAuthPlugin {
const realm = options.realm ?? "api"
const optional = options.optional === true
const store = new WeakMap
}