Created
September 9, 2025 18:55
-
-
Save webstrand/14970758c78bcd6f5eab4d61102dac54 to your computer and use it in GitHub Desktop.
Plugin for adding basic auth to vite
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
| import { type Plugin } from "vite"; | |
| import { default as parseBasicAuth, type BasicAuthResult } from "basic-auth"; | |
| import { createHash, timingSafeEqual, randomBytes } from "node:crypto"; | |
| import { type IncomingMessage } from "node:http"; | |
| const HASH_FUNCTION = "sha256"; | |
| const HASH_SIZE = createHash(HASH_FUNCTION).digest().length; | |
| const SALT_SIZE = 16; | |
| export type HashedPassword = readonly [hash: Buffer, salt: Buffer]; | |
| /** | |
| * Map of usernames to {@link HashedPassword}s. Use {@link hashPassword} for the values | |
| */ | |
| export type Credentials = ReadonlyMap<string, HashedPassword | null | undefined>; | |
| /** | |
| * Function that returns {@link Credentials} for a given request or a control signal | |
| * @param req The incoming HTTP request, it is unsafe to parse `Authorization` | |
| * @returns A {@link Credentials} map to authenticate access, `true` to allow access, | |
| * or `false | null | undefined` to deny access | |
| */ | |
| export type CredentialsFunction = (req: IncomingMessage) => Credentials | boolean | null | undefined; | |
| /** | |
| * Hash a password for inclusion in {@link Credentials} | |
| * @param str The plaintext password to hash | |
| * @returns `[hash, salt]` structure | |
| */ | |
| export function hashPassword(str: string): HashedPassword { | |
| const salt = randomBytes(SALT_SIZE); | |
| return Object.freeze([internalHashPassword(str, salt), salt]); | |
| } | |
| function internalHashPassword(str: string, salt: Buffer): Buffer { | |
| return createHash(HASH_FUNCTION).update(str).update(salt).digest(); | |
| } | |
| /** | |
| * Vite plugin that adds HTTP Basic Authentication to the dev server. | |
| * @param config Configuration object | |
| * @returns Vite plugin instance | |
| */ | |
| export function basicAuth({ | |
| credentials, | |
| realm, | |
| }: { | |
| /** Static credentials map or function that returns credentials per request */ | |
| credentials: Credentials | CredentialsFunction; | |
| /** Realm name shown in browser auth dialog. If null/undefined, no challenge header is sent */ | |
| realm: string | null | undefined; | |
| }): Plugin { | |
| if (realm != null) { | |
| if (/[\x00-\x1F\x7F]/.test(realm)) throw new Error("Invalid characters in realm name"); | |
| realm = realm.replace(/[\\"]/g, "\\$&"); | |
| } | |
| // Placeholder to compare against in case username does not match | |
| // this helps to prevent timing attacks exfiltrating usernames lists. | |
| const dummyHashedPassword: HashedPassword = Object.freeze([randomBytes(HASH_SIZE), randomBytes(SALT_SIZE)]); | |
| function verifyPassword(valid: Credentials, request: BasicAuthResult) { | |
| const hashedPassword = valid.get(request.name) ?? dummyHashedPassword; | |
| const hash = internalHashPassword(request.pass, hashedPassword[1]); | |
| return timingSafeEqual(hash, hashedPassword[0]); | |
| } | |
| return { | |
| name: "basic-auth", | |
| configureServer(server) { | |
| server.middlewares.use(function (req, res, next) { | |
| const valid = typeof credentials === "function" ? credentials(req) : credentials; | |
| if (valid === true) return next(); | |
| const reqCred = parseBasicAuth(req); | |
| if (valid && reqCred && verifyPassword(valid, reqCred)) return next(); | |
| res.statusCode = 401; | |
| if (valid && realm != null) res.setHeader("WWW-Authenticate", `Basic realm="${realm}"`); | |
| res.end(valid && reqCred ? "Authentication Failure" : undefined); | |
| return; | |
| }); | |
| }, | |
| }; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment