Harden JWT auth: require strong secret, expire tokens
Security review flagged a hardcoded JWT secret fallback. A booth machine started without JWT_SECRET would have signed tokens with a publicly-known default, letting anyone forge an admin token — defeating the local-auth anti-fraud model. - requireJwtSecret() refuses to start on a missing, <32-char, or placeholder secret (no insecure default). - Add sign.expiresIn: 8h so minted tokens expire (bound to a shift). - Add apps/server/.env.example documenting JWT_SECRET + how to generate it. Verified: refuses with no secret and with the old placeholder; boots and serves /health with a valid `openssl rand -hex 32` secret.
This commit is contained in:
@@ -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
|
||||
@@ -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<FastifyInstance> {
|
||||
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" }));
|
||||
|
||||
Reference in New Issue
Block a user