diff --git a/apps/server/.env.example b/apps/server/.env.example new file mode 100644 index 0000000..6309338 --- /dev/null +++ b/apps/server/.env.example @@ -0,0 +1,11 @@ +# Copy to .env and fill in. The server refuses to start without a strong JWT_SECRET. +# +# Generate a strong secret: +# openssl rand -hex 32 +JWT_SECRET= + +# Optional +# PORT=3000 +# HOST=0.0.0.0 +# LOG_LEVEL=info +# DATABASE_URL=./parking.sqlite diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index aa08fb8..b03a988 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -13,14 +13,34 @@ declare module "@fastify/jwt" { } } +/** + * Resolve the JWT signing secret, refusing to start without a strong one. + * There is deliberately no fallback default — a missing, short, or placeholder + * secret throws so the server never runs with forgeable tokens. + */ +function requireJwtSecret(): string { + const secret = process.env.JWT_SECRET; + if (!secret || secret.length < 32 || /change.?me|insecure|dev-only/i.test(secret)) { + throw new Error( + "JWT_SECRET must be set to a strong random value (>=32 chars). " + + "Generate one with: openssl rand -hex 32", + ); + } + return secret; +} + export async function buildServer(): Promise { const app = Fastify({ logger: { level: process.env.LOG_LEVEL ?? "info" }, }); // Local JWT signing with a local secret — no external identity provider. + // Fail fast rather than fall back to a known default: a booth machine started + // without a real secret would sign tokens anyone could forge (incl. an admin + // token), defeating the whole local-auth/anti-fraud model. No insecure default. await app.register(jwt, { - secret: process.env.JWT_SECRET ?? "dev-only-insecure-secret-change-me", + secret: requireJwtSecret(), + sign: { expiresIn: "8h" }, // bound to a shift; minted tokens must expire }); app.get("/health", async () => ({ status: "ok" }));