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> {
|
export async function buildServer(): Promise<FastifyInstance> {
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
logger: { level: process.env.LOG_LEVEL ?? "info" },
|
logger: { level: process.env.LOG_LEVEL ?? "info" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Local JWT signing with a local secret — no external identity provider.
|
// 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, {
|
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" }));
|
app.get("/health", async () => ({ status: "ok" }));
|
||||||
|
|||||||
Reference in New Issue
Block a user