From 32e7036b8a58cabb86febfbaf2bf98d32d30e56c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:13:12 -0700 Subject: [PATCH] Use hash comparison for pw --- .../pages/api/auth/[...nextauth].test.js | 22 +++++++++++++++++++ src/pages/api/auth/[...nextauth].js | 13 ++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/__tests__/pages/api/auth/[...nextauth].test.js b/src/__tests__/pages/api/auth/[...nextauth].test.js index a2a56f7f4..7694f2b08 100644 --- a/src/__tests__/pages/api/auth/[...nextauth].test.js +++ b/src/__tests__/pages/api/auth/[...nextauth].test.js @@ -140,6 +140,28 @@ describe("pages/api/auth/[...nextauth]", () => { expect(provider.type).toBe("credentials"); expect(typeof provider.authorize).toBe("function"); expect(mod.default.options.useSecureCookies).toBe(true); + await expect(provider.options.authorize({ password: "secret" })).resolves.toEqual({ + id: "homepage", + name: "Homepage", + }); + await expect(provider.options.authorize({ password: "wrong" })).resolves.toBeNull(); + await expect(provider.options.authorize({ password: 123 })).resolves.toBeNull(); + }); + + it("compares multibyte passwords without throwing on unequal byte lengths", async () => { + process.env.HOMEPAGE_AUTH_ENABLED = "true"; + process.env.HOMEPAGE_AUTH_PASSWORD = "é"; + process.env.HOMEPAGE_AUTH_SECRET = "auth-secret"; + process.env.HOMEPAGE_EXTERNAL_URL = "https://homepage.example"; + + const mod = await import("pages/api/auth/[...nextauth]"); + const [provider] = mod.default.options.providers; + + await expect(provider.options.authorize({ password: "a" })).resolves.toBeNull(); + await expect(provider.options.authorize({ password: "é" })).resolves.toEqual({ + id: "homepage", + name: "Homepage", + }); }); it("supports trusted HTTP deployments without Secure cookies", async () => { diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js index 1b7c8e180..2ada235d0 100644 --- a/src/pages/api/auth/[...nextauth].js +++ b/src/pages/api/auth/[...nextauth].js @@ -1,4 +1,4 @@ -import { timingSafeEqual } from "node:crypto"; +import { createHash, timingSafeEqual } from "node:crypto"; import NextAuth from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; @@ -12,6 +12,9 @@ const clientSecret = process.env.HOMEPAGE_OIDC_CLIENT_SECRET; const homepageAuthSecret = process.env.HOMEPAGE_AUTH_SECRET; const homepageExternalUrl = process.env.HOMEPAGE_EXTERNAL_URL; const homepageAuthPassword = process.env.HOMEPAGE_AUTH_PASSWORD; +const homepageAuthPasswordDigest = homepageAuthPassword + ? createHash("sha256").update(homepageAuthPassword, "utf8").digest() + : null; // Map HOMEPAGE_* envs to what NextAuth expects if (!process.env.NEXTAUTH_SECRET && homepageAuthSecret) { @@ -97,12 +100,12 @@ if (authEnabled) { password: { label: "Password", type: "password" }, }, async authorize(credentials) { - const provided = credentials?.password ?? ""; - const expected = homepageAuthPassword ?? ""; - if (!expected || provided.length !== expected.length) { + const provided = credentials?.password; + if (!homepageAuthPasswordDigest || typeof provided !== "string") { return null; } - const isMatch = timingSafeEqual(Buffer.from(provided), Buffer.from(expected)); + const providedDigest = createHash("sha256").update(provided, "utf8").digest(); + const isMatch = timingSafeEqual(providedDigest, homepageAuthPasswordDigest); if (!isMatch) { return null; }