Skip to content

Commit 2337e2e

Browse files
committed
refactor(auth): remove nonce, rely on rate limiting + JWT
1 parent a5c70e0 commit 2337e2e

File tree

3 files changed

+13
-93
lines changed

3 files changed

+13
-93
lines changed

deploy/Caddyfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
}
44

55
:8080 {
6-
# HTML page: Caddy templates inject base path + session nonce
6+
# HTML page: Caddy templates inject base path
77
@htmlpage {
88
path /
99
path /index.html

deploy/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ COPY frontend/package.json frontend/pnpm-lock.yaml ./frontend/
1313
RUN cd frontend && pnpm install --frozen-lockfile
1414
COPY frontend/ ./frontend/
1515
RUN cd frontend && pnpm build
16-
# Inject Caddy template directives for subpath base path + session nonce
17-
RUN sed -i 's|<head>|<head>\n <base href="{{ .Req.Header.Get "X-Base-Path" }}">\n <meta name="session-nonce" content="{{ placeholder "http.request.uuid" }}">|' ./frontend/dist/index.html
16+
# Inject Caddy template directive for subpath base path
17+
RUN sed -i 's|<head>|<head>\n <base href="{{ .Req.Header.Get "X-Base-Path" }}">|' ./frontend/dist/index.html
1818

1919
# Stage 3: Runtime
2020
FROM node:24-slim

server.js

Lines changed: 10 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* - Accepts text in body and model as query parameter
1111
* - Returns binary audio data (application/octet-stream)
1212
* - CORS enabled for frontend communication
13-
* - JWT session auth with page nonce (production only)
13+
* - JWT session auth with rate limiting (production only)
1414
* - Pure API server (frontend served separately)
1515
*/
1616

@@ -44,70 +44,18 @@ const CONFIG = {
4444
};
4545

4646
// ============================================================================
47-
// SESSION AUTH - JWT tokens with page nonce for production security
47+
// SESSION AUTH - JWT tokens for production security
4848
// ============================================================================
4949

5050
/**
51-
* Session secret for signing JWTs. When set (production/Fly.io), nonce
52-
* validation is enforced. When unset (local dev), tokens are issued freely.
51+
* Session secret for signing JWTs.
5352
*/
5453
const SESSION_SECRET =
5554
process.env.SESSION_SECRET || crypto.randomBytes(32).toString("hex");
56-
const REQUIRE_NONCE = !!process.env.SESSION_SECRET;
57-
58-
/** In-memory nonce store: nonce → expiry timestamp */
59-
const sessionNonces = new Map();
60-
61-
/** Nonce expiry time (5 minutes) */
62-
const NONCE_TTL_MS = 5 * 60 * 1000;
6355

6456
/** JWT expiry time (1 hour) */
6557
const JWT_EXPIRY = "1h";
6658

67-
/**
68-
* Generates a single-use nonce and stores it with an expiry
69-
* @returns {string} The generated nonce
70-
*/
71-
function generateNonce() {
72-
const nonce = crypto.randomBytes(16).toString("hex");
73-
sessionNonces.set(nonce, Date.now() + NONCE_TTL_MS);
74-
return nonce;
75-
}
76-
77-
/**
78-
* Validates and consumes a nonce (single-use)
79-
* @param {string} nonce - The nonce to validate
80-
* @returns {boolean} True if the nonce was valid and consumed
81-
*/
82-
function consumeNonce(nonce) {
83-
const expiry = sessionNonces.get(nonce);
84-
if (!expiry) return false;
85-
sessionNonces.delete(nonce);
86-
return Date.now() < expiry;
87-
}
88-
89-
/** Periodically clean up expired nonces (every 60 seconds) */
90-
setInterval(() => {
91-
const now = Date.now();
92-
for (const [nonce, expiry] of sessionNonces) {
93-
if (now >= expiry) sessionNonces.delete(nonce);
94-
}
95-
}, 60_000);
96-
97-
/**
98-
* Reads frontend/dist/index.html and injects a session nonce meta tag.
99-
* Returns null in dev mode (no built frontend).
100-
*/
101-
let indexHtmlTemplate = null;
102-
try {
103-
indexHtmlTemplate = fs.readFileSync(
104-
path.join(__dirname, "frontend", "dist", "index.html"),
105-
"utf-8"
106-
);
107-
} catch {
108-
// No built frontend (dev mode) — index.html served by Vite
109-
}
110-
11159
/**
11260
* Express middleware that validates JWT from Authorization header.
11361
* Returns 401 with JSON error if token is missing or invalid.
@@ -305,42 +253,14 @@ function formatErrorResponse(error, statusCode = 500, errorCode = null) {
305253
// ============================================================================
306254

307255
/**
308-
* GET / — Serve index.html with injected session nonce (production only).
309-
* In dev mode, Vite serves the frontend directly.
310-
*/
311-
app.get("/", (req, res) => {
312-
if (!indexHtmlTemplate) {
313-
return res.status(404).send("Frontend not built. Run make build first.");
314-
}
315-
const nonce = generateNonce();
316-
const html = indexHtmlTemplate.replace(
317-
"</head>",
318-
`<meta name="session-nonce" content="${nonce}">\n</head>`
319-
);
320-
res.type("html").send(html);
321-
});
322-
323-
/**
324-
* GET /api/session — Issues a JWT. In production (SESSION_SECRET set),
325-
* requires a valid single-use nonce via X-Session-Nonce header.
256+
* GET /api/session — Issues a signed JWT for session authentication.
326257
*/
327258
app.get("/api/session", (req, res) => {
328-
if (REQUIRE_NONCE) {
329-
const nonce = req.headers["x-session-nonce"];
330-
if (!nonce || !consumeNonce(nonce)) {
331-
return res.status(403).json({
332-
error: {
333-
type: "AuthenticationError",
334-
code: "INVALID_NONCE",
335-
message: "Valid session nonce required. Please refresh the page.",
336-
},
337-
});
338-
}
339-
}
340-
341-
const token = jwt.sign({ iat: Math.floor(Date.now() / 1000) }, SESSION_SECRET, {
342-
expiresIn: JWT_EXPIRY,
343-
});
259+
const token = jwt.sign(
260+
{ iat: Math.floor(Date.now() / 1000) },
261+
SESSION_SECRET,
262+
{ expiresIn: JWT_EXPIRY }
263+
);
344264
res.json({ token });
345265
});
346266

@@ -478,7 +398,7 @@ app.get("/api/metadata", (req, res) => {
478398
app.listen(CONFIG.port, CONFIG.host, () => {
479399
console.log("\n" + "=".repeat(70));
480400
console.log(`🚀 Backend API running at http://localhost:${CONFIG.port}`);
481-
console.log(`📡 GET /api/session${REQUIRE_NONCE ? " (nonce required)" : ""}`);
401+
console.log(`📡 GET /api/session`);
482402
console.log(`📡 POST /api/text-to-speech (auth required)`);
483403
console.log(`📡 GET /api/metadata`);
484404
console.log("=".repeat(70) + "\n");

0 commit comments

Comments
 (0)