Skip to content

Commit d8b968d

Browse files
committed
refactor(auth): remove nonce, rely on rate limiting + JWT
1 parent 4e191f0 commit d8b968d

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
@@ -9,7 +9,7 @@
99
* - Accepts text or URL in JSON body
1010
* - Supports multiple intelligence features: summarization, topics, sentiment, intents
1111
* - CORS-enabled for frontend communication
12-
* - JWT session auth with page nonce (production only)
12+
* - JWT session auth with rate limiting (production only)
1313
*/
1414

1515
require("dotenv").config();
@@ -33,70 +33,18 @@ const CONFIG = {
3333
};
3434

3535
// ============================================================================
36-
// SESSION AUTH - JWT tokens with page nonce for production security
36+
// SESSION AUTH - JWT tokens for production security
3737
// ============================================================================
3838

3939
/**
40-
* Session secret for signing JWTs. When set (production/Fly.io), nonce
41-
* validation is enforced. When unset (local dev), tokens are issued freely.
40+
* Session secret for signing JWTs.
4241
*/
4342
const SESSION_SECRET =
4443
process.env.SESSION_SECRET || crypto.randomBytes(32).toString("hex");
45-
const REQUIRE_NONCE = !!process.env.SESSION_SECRET;
46-
47-
/** In-memory nonce store: nonce -> expiry timestamp */
48-
const sessionNonces = new Map();
49-
50-
/** Nonce expiry time (5 minutes) */
51-
const NONCE_TTL_MS = 5 * 60 * 1000;
5244

5345
/** JWT expiry time (1 hour) */
5446
const JWT_EXPIRY = "1h";
5547

56-
/**
57-
* Generates a single-use nonce and stores it with an expiry
58-
* @returns {string} The generated nonce
59-
*/
60-
function generateNonce() {
61-
const nonce = crypto.randomBytes(16).toString("hex");
62-
sessionNonces.set(nonce, Date.now() + NONCE_TTL_MS);
63-
return nonce;
64-
}
65-
66-
/**
67-
* Validates and consumes a nonce (single-use)
68-
* @param {string} nonce - The nonce to validate
69-
* @returns {boolean} True if the nonce was valid and consumed
70-
*/
71-
function consumeNonce(nonce) {
72-
const expiry = sessionNonces.get(nonce);
73-
if (!expiry) return false;
74-
sessionNonces.delete(nonce);
75-
return Date.now() < expiry;
76-
}
77-
78-
/** Periodically clean up expired nonces (every 60 seconds) */
79-
setInterval(() => {
80-
const now = Date.now();
81-
for (const [nonce, expiry] of sessionNonces) {
82-
if (now >= expiry) sessionNonces.delete(nonce);
83-
}
84-
}, 60_000);
85-
86-
/**
87-
* Reads frontend/dist/index.html and injects a session nonce meta tag.
88-
* Returns null in dev mode (no built frontend).
89-
*/
90-
let indexHtmlTemplate = null;
91-
try {
92-
indexHtmlTemplate = fs.readFileSync(
93-
path.join(__dirname, "frontend", "dist", "index.html"),
94-
"utf-8"
95-
);
96-
} catch {
97-
// No built frontend (dev mode) — index.html served by Vite
98-
}
99-
10048
/**
10149
* Express middleware that validates JWT from Authorization header.
10250
* Returns 401 with JSON error if token is missing or invalid.
@@ -168,42 +116,14 @@ app.use(cors());
168116
// ============================================================================
169117

170118
/**
171-
* GET / — Serve index.html with injected session nonce (production only).
172-
* In dev mode, Vite serves the frontend directly.
173-
*/
174-
app.get("/", (req, res) => {
175-
if (!indexHtmlTemplate) {
176-
return res.status(404).send("Frontend not built. Run make build first.");
177-
}
178-
const nonce = generateNonce();
179-
const html = indexHtmlTemplate.replace(
180-
"</head>",
181-
`<meta name="session-nonce" content="${nonce}">\n</head>`
182-
);
183-
res.type("html").send(html);
184-
});
185-
186-
/**
187-
* GET /api/session — Issues a JWT. In production (SESSION_SECRET set),
188-
* requires a valid single-use nonce via X-Session-Nonce header.
119+
* GET /api/session — Issues a signed JWT for session authentication.
189120
*/
190121
app.get("/api/session", (req, res) => {
191-
if (REQUIRE_NONCE) {
192-
const nonce = req.headers["x-session-nonce"];
193-
if (!nonce || !consumeNonce(nonce)) {
194-
return res.status(403).json({
195-
error: {
196-
type: "AuthenticationError",
197-
code: "INVALID_NONCE",
198-
message: "Valid session nonce required. Please refresh the page.",
199-
},
200-
});
201-
}
202-
}
203-
204-
const token = jwt.sign({ iat: Math.floor(Date.now() / 1000) }, SESSION_SECRET, {
205-
expiresIn: JWT_EXPIRY,
206-
});
122+
const token = jwt.sign(
123+
{ iat: Math.floor(Date.now() / 1000) },
124+
SESSION_SECRET,
125+
{ expiresIn: JWT_EXPIRY }
126+
);
207127
res.json({ token });
208128
});
209129

@@ -432,7 +352,7 @@ app.get('/api/metadata', (req, res) => {
432352
app.listen(CONFIG.port, CONFIG.host, () => {
433353
console.log("\n" + "=".repeat(70));
434354
console.log(`🚀 Backend API running at http://localhost:${CONFIG.port}`);
435-
console.log(`📡 GET /api/session${REQUIRE_NONCE ? " (nonce required)" : ""}`);
355+
console.log(`📡 GET /api/session`);
436356
console.log(`📡 POST /api/text-intelligence (auth required)`);
437357
console.log(`📡 GET /api/metadata`);
438358
console.log("=".repeat(70) + "\n");

0 commit comments

Comments
 (0)