diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index c30fbb2..3695d3c 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -1,6 +1,7 @@ import { Module } from "@nestjs/common" import { CacheModule } from "@nestjs/cache-manager" import { JwtModule } from "@nestjs/jwt" +import createJwtConfig from "../config/jwt.config" import { AuthController } from "./auth.controller" import { AuthService } from "./auth.service" import { TokenDenylistService } from "./token-denylist.service" @@ -15,9 +16,8 @@ const JWT_EXPIRES_IN = "15m" ttl: 3600, max: 1024, }), - JwtModule.register({ - secret: process.env.JWT_SECRET ?? "dev-secret-change-me", - signOptions: { expiresIn: JWT_EXPIRES_IN }, + JwtModule.registerAsync({ + useFactory: () => createJwtConfig(JWT_EXPIRES_IN), }), ], controllers: [AuthController], diff --git a/api/src/config/env.ts b/api/src/config/env.ts index 2d9b2fd..339bfa6 100644 --- a/api/src/config/env.ts +++ b/api/src/config/env.ts @@ -4,7 +4,8 @@ const envSchema = z.object({ PORT: z.string().default("3001"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), DATABASE_URL: z.string().min(1, "DATABASE_URL is required"), - JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), + // JWT_SECRET is required in production and test, but optional in development + JWT_SECRET: z.string().optional(), STREAM_API_KEY: z.string().min(1, "STREAM_API_KEY is required"), }) @@ -19,6 +20,12 @@ export function validateEnv(): Env { console.error(`Environment validation failed:\n${errors}`) process.exit(1) } + // Enforce JWT secret presence for non-development environments + if (result.data.NODE_ENV !== "development" && !result.data.JWT_SECRET) { + console.error("Environment validation failed:\n - JWT_SECRET: JWT_SECRET is required in non-development environments") + process.exit(1) + } + return result.data } diff --git a/api/src/config/jwt.config.ts b/api/src/config/jwt.config.ts new file mode 100644 index 0000000..a933882 --- /dev/null +++ b/api/src/config/jwt.config.ts @@ -0,0 +1,24 @@ +import { JwtModuleOptions } from "@nestjs/jwt" +import { randomBytes } from "crypto" +import { env } from "./env" + +export function createJwtConfig(expiresIn = "1h"): JwtModuleOptions { + // Prefer the explicitly set env var, but fall back to validated env if present + const secret = process.env.JWT_SECRET ?? env.JWT_SECRET + + if (!secret) { + if (env.NODE_ENV === "development") { + const generated = randomBytes(32).toString("hex") + console.warn( + "WARNING: No JWT_SECRET set; generating a random secret for development only. This is INSECURE for production." + ) + console.warn(`Generated development JWT secret: ${generated}`) + return { secret: generated, signOptions: { expiresIn } } + } + throw new Error("JWT_SECRET must be set") + } + + return { secret, signOptions: { expiresIn } } +} + +export default createJwtConfig diff --git a/api/src/gateways/gateways.module.ts b/api/src/gateways/gateways.module.ts index 8c55062..a9572a0 100644 --- a/api/src/gateways/gateways.module.ts +++ b/api/src/gateways/gateways.module.ts @@ -1,5 +1,6 @@ import { Module } from "@nestjs/common" import { JwtModule } from "@nestjs/jwt" +import createJwtConfig from "../config/jwt.config" import { MetricsModule } from "../metrics/metrics.module" import { StreamsGateway } from "./streams.gateway" @@ -13,18 +14,7 @@ import { StreamsGateway } from "./streams.gateway" imports: [ MetricsModule, JwtModule.registerAsync({ - useFactory: () => { - const secret = process.env.JWT_SECRET - if (!secret && process.env.NODE_ENV === "production") { - throw new Error( - "JWT_SECRET must be set in production for WebSocket auth", - ) - } - return { - secret: secret ?? "dev-insecure-secret-change-me", - signOptions: { expiresIn: "1h" }, - } - }, + useFactory: () => createJwtConfig("1h"), }), ], providers: [StreamsGateway],