Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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],
Expand Down
9 changes: 8 additions & 1 deletion api/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
})

Expand All @@ -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
}

Expand Down
24 changes: 24 additions & 0 deletions api/src/config/jwt.config.ts
Original file line number Diff line number Diff line change
@@ -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
14 changes: 2 additions & 12 deletions api/src/gateways/gateways.module.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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],
Expand Down
Loading