Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const crypto = require("crypto");
const { rateLimit } = require("express-rate-limit");
const slowDown = require("express-slow-down");
const helmet = require("helmet");
const jwt = require("jsonwebtoken");
const { askSchema, summarizeSchema, knowledgeGapsSchema, sessionsLookupSchema } = require("./validators/schemas");
const { clientIpFromRequest } = require("./security/ip");
const { createRedisClient } = require("./security/redis");
Expand All @@ -18,6 +19,7 @@ const authRoutes = require("./src/routes/authRoutes");
const RAG_SERVICE_URL = process.env.RAG_SERVICE_URL || "http://localhost:5000";
const getInternalRagToken = () => (process.env.INTERNAL_RAG_TOKEN || "").trim();
const PORT = process.env.PORT || 4000;
const SUPABASE_JWT_SECRET = (process.env.SUPABASE_JWT_SECRET || "").trim();

const app = express();

Expand Down Expand Up @@ -720,20 +722,18 @@ const requireSupabaseAuth = (req, res, next) => {
}

const token = authHeader.split(" ")[1];
const secret = process.env.SUPABASE_JWT_SECRET;

// If the server admin hasn't configured the JWT secret, we at least enforce
// that a token is provided (to satisfy basic security checks), but we can't
// cryptographically verify it without the secret.
if (secret) {
const jwt = require("jsonwebtoken");
try {
req.user = jwt.verify(token, secret);
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
const secret = SUPABASE_JWT_SECRET;

if (!secret) {
return res.status(500).json({ error: "Server misconfiguration: missing SUPABASE_JWT_SECRET" });
}
Comment on lines +727 to +729


try {
req.user = jwt.verify(token, secret);
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
Comment on lines +727 to +734
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the exact region around the reported lines
sed -n '700,780p' server.js | nl -ba

# Find all usages of req.user to understand what claims are relied on
rg -n "req\.user" server.js

# Find where token is sourced and what middleware this snippet belongs to
rg -n "const token|token\s*=" server.js
rg -n "Authorization|bearer|req\.headers" server.js

# Show surrounding auth middleware definition (helps confirm expectations)
rg -n "verify|jsonwebtoken|SUPABASE_JWT_SECRET" server.js

Repository: FireFistisDead/pdf-qa-bot

Length of output: 112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm file exists
ls -la server.js

# Show the exact region around the reported lines with line numbers (no `nl` needed)
sed -n '700,780p' server.js | awk '{printf "%6d %s\n", NR+699, $0}'

# Find all usages of req.user to understand what claims are relied on
rg -n "req\.user" server.js

# Find where token is sourced and what middleware this snippet belongs to
rg -n "(token\s*=|const token|Authorization|bearer|req\.headers)" server.js

# Find where jsonwebtoken is used and where SUPABASE_JWT_SECRET is referenced
rg -n "(jsonwebtoken|jwt\.verify|SUPABASE_JWT_SECRET)" server.js

# Show any claim/role/aud checks nearby (if any)
rg -n "(role|aud|sub|authenticated|service_role|anon)" server.js

Repository: FireFistisDead/pdf-qa-bot

Length of output: 4551


🌐 Web query:

Supabase JWT claims role aud anon service_role authenticated

💡 Result:

In Supabase Auth JWTs, the relevant claims are: 1) role - "anon": anonymous user - "authenticated": authenticated user - "service_role": service role (intended for server-side use) [1] 2) aud (audience) - "authenticated": for authenticated user tokens - "anon": for anonymous user tokens [1] So the typical combinations you’ll see are: - Anonymous access: role = "anon" and aud = "anon" [1] - Authenticated user access: role = "authenticated" and aud = "authenticated" [1] - Service role: role = "service_role" (service-role token example shown includes role="service_role"; aud is part of the required JWT claims schema but the key point is the role value) [1] If you’re decoding/inspecting JWTs: Supabase documents these as standard JWT claims used for authorization with Postgres/RLS via the role claim [2].

Citations:


🌐 Web query:

Supabase JWT signing algorithm HS256 RS256 project JWT secret jwt.verify algorithms

💡 Result:

Supabase supports multiple algorithms for signing and verifying JSON Web Tokens (JWTs), but it strongly recommends using asymmetric algorithms over symmetric ones [1][2]. Asymmetric Algorithms (Recommended) Supabase supports asymmetric signing keys, which provide improved security and performance compared to shared secrets [1][3]. - ES256 (NIST P-256 Curve): This is the recommended algorithm. It is faster than RSA and produces shorter signatures, which is beneficial for reducing data transfer sizes and cookie management [1][4]. - RS256 (RSA 2048): A widely supported public-key algorithm, though generally slower than elliptic curves [1][4]. Symmetric Algorithm (Not Recommended for Production) - HS256 (HMAC with SHA-256): This uses a shared secret [1][5]. Supabase explicitly advises against using HS256 for production applications because there is no public key counterpart, making key revocation more difficult and potentially exposing your project to security risks [1][4][6]. JWT Verification - Preferred Method: For asymmetric keys, Supabase recommends using supabase.auth.getClaims() or high-quality JWT verification libraries [2][3]. These libraries verify the token locally using public keys fetched from your project's JWKS endpoint (/.well-known/jwks.json), avoiding the need to make network calls to the Auth server [2][3][7]. - Legacy/HS256 Verification: If you are still using a shared secret (HS256), the recommended approach is to verify the token by sending a request to the Auth server (e.g., via getUser()), rather than verifying it locally with the secret [2][6]. Security Best Practices - Algorithm Pinning: Always pin the expected algorithm in your verification code (e.g., algorithms: ["RS256"]) to prevent algorithm confusion attacks, where an attacker might attempt to force a symmetric signature using an asymmetric public key [8][5]. - Avoid alg: none: Explicitly reject tokens with the none algorithm [5][9]. - Use JWKS: Leverage the public JSON Web Key Set (JWKS) endpoint provided by your project for secure, automated public key discovery when using asymmetric algorithms [2][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for SUPABASE_JWT_SECRET usage and any documentation/comments about JWT algorithm/role handling
rg -n "SUPABASE_JWT_SECRET|jwt\.verify|jsonwebtoken|role\"|aud\"|service_role|anon\"|authenticated\"" -S .

# Show server.js around token verification (more context)
sed -n '680,760p' server.js

# Locate any config/env/doc mentioning JWT algorithm (HS256/RS256) or Supabase Auth JWT structure
rg -n "HS256|RS256|JWKS|well-known/jwks|getClaims|getUser|supabase\.auth" -S . || true

# List repository top-level files that might contain auth instructions
ls -ლა

Repository: FireFistisDead/pdf-qa-bot

Length of output: 50465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' src/middleware/authMiddleware.js
echo "----"
sed -n '1,220p' src/controllers/authController.js
echo "----"
sed -n '1,140p' server.test.js
echo "----"
rg -n "authMiddleware|requireSupabaseAuth|req\.user|jwt\.verify" -S server.js src/middleware src/controllers server.test.js
echo "----"
rg -n "role\"|aud\"|service_role|anon\"|authenticated\"|sub\"" -S server.js src/middleware src/controllers

Repository: FireFistisDead/pdf-qa-bot

Length of output: 8088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where /process-from-url is called and what Authorization header is set
rg -n "process-from-url" -S . --glob='*.js' --glob='*.jsx' --glob='*.ts' --glob='*.tsx'

rg -n "SUPABASE_JWT_SECRET|requireSupabaseAuth|Bearer " -S server.js frontend src --glob='*.js' --glob='*.jsx' --glob='*.ts' --glob='*.tsx'

# Locate ragAuthHeaders implementation (to ensure it's not being conflated with Supabase JWT auth)
rg -n "function ragAuthHeaders|ragAuthHeaders" -S server.js src --glob='*.js'

# Check if `req.user` is used anywhere besides assignment
rg -n "req\.user" server.js src --glob='*.js'

Repository: FireFistisDead/pdf-qa-bot

Length of output: 1899


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for any documentation / config references
rg -n "SUPABASE_JWT_SECRET|process-from-url|requireSupabaseAuth|access_token|role\"|aud\"" -S . --glob='README*' --glob='*.md' --glob='*.js' --glob='*.ts'

# Show the frontend call site with surrounding context (ragService.js)
sed -n '1,120p' frontend/src/services/ragService.js

# Extract any comments around /process-from-url auth expectations
rg -n "process-from-url" -n server.js
sed -n '700,900p' server.js

Repository: FireFistisDead/pdf-qa-bot

Length of output: 12346


Enforce Supabase JWT claim checks in requireSupabaseAuth (reject role=anon / non-user tokens)

requireSupabaseAuth only does jwt.verify(token, secret) and assigns the result to req.user without checking Supabase authorization claims (role / aud). That means an anon (and potentially service_role) JWT can pass this middleware.

After jwt.verify, require user-level claims, e.g. claims.role === "authenticated" (and typically claims.aud === "authenticated"), otherwise return 401. See Supabase JWT fields: https://supabase.com/docs/guides/auth/jwt-fields

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 729 - 738, After verifying the JWT in
requireSupabaseAuth, validate Supabase-specific claims on the decoded token (the
object assigned to req.user from jwt.verify) and reject non-user tokens: ensure
req.user.role === "authenticated" (and optionally req.user.aud ===
"authenticated") and return a 401 JSON error if these checks fail; keep the
jwt.verify(token, secret) call but add explicit claim checks to prevent anon or
service_role tokens from being accepted.

}

next();
};

Expand Down Expand Up @@ -1058,6 +1058,9 @@ app.use((err, req, res, next) => {

if (require.main === module) {
requireInternalRagToken();
if (!SUPABASE_JWT_SECRET) {
throw new Error("SUPABASE_JWT_SECRET missing in .env – required for /process-from-url authentication");
}
Comment on lines 1060 to +1063

(async () => {
requireInternalRagToken();
Expand Down
156 changes: 156 additions & 0 deletions server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ const http = require("node:http");
const { spawnSync } = require("node:child_process");
const axios = require("axios");
const { Blob } = require("node:buffer");
const jwt = require("jsonwebtoken");

const originalInternalRagToken = process.env.INTERNAL_RAG_TOKEN;
const originalJwtSecret = process.env.JWT_SECRET;
const originalSupabaseJwtSecret = process.env.SUPABASE_JWT_SECRET;

before(() => {
process.env.INTERNAL_RAG_TOKEN = process.env.INTERNAL_RAG_TOKEN || "test-internal-rag-token";
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-jwt-secret";
process.env.SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET || "test-supabase-jwt-secret";
});

after(() => {
Expand All @@ -25,6 +28,12 @@ after(() => {
} else {
process.env.JWT_SECRET = originalJwtSecret;
}

if (originalSupabaseJwtSecret === undefined) {
delete process.env.SUPABASE_JWT_SECRET;
} else {
process.env.SUPABASE_JWT_SECRET = originalSupabaseJwtSecret;
}
});

// Module-load test: would throw at require time if any undefined
Expand Down Expand Up @@ -92,6 +101,27 @@ test("server startup fails when INTERNAL_RAG_TOKEN is unset", () => {
assert.match(`${result.stderr}${result.stdout}`, /INTERNAL_RAG_TOKEN must be configured/);
});

test("server startup fails when SUPABASE_JWT_SECRET is unset", () => {
const result = spawnSync(
process.execPath,
["server.js"],
{
cwd: __dirname,
env: {
...process.env,
SUPABASE_JWT_SECRET: "",
JWT_SECRET: "test-jwt-secret",
INTERNAL_RAG_TOKEN: "test-internal-rag-token",
},
encoding: "utf8",
timeout: 5000,
},
);

assert.notEqual(result.status, 0);
assert.match(`${result.stderr}${result.stdout}`, /SUPABASE_JWT_SECRET missing/);
});

const createPdfUploadBody = ({ sessionId = null, sessionSecret = null } = {}) => {
const formData = new FormData();
formData.append(
Expand Down Expand Up @@ -689,3 +719,129 @@ describe("route error responses", () => {
assert.ok(data.token);
});
});

describe("requireSupabaseAuth", () => {
let server;
let baseUrl;

before(() => {
return new Promise((resolve) => {
server = http.createServer(app);
server.listen(0, () => {
const address = server.address();
baseUrl = `http://127.0.0.1:${address.port}`;
resolve();
});
});
});

after(() => {
if (server) server.close();
});

test("rejects missing Authorization header", async () => {
const res = await fetch(`${baseUrl}/process-from-url`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: "https://example.com/test.pdf" }),
});
assert.equal(res.status, 401);
const data = await res.json();
assert.equal(data.error, "Missing or invalid authorization token");
});

test("rejects malformed token", async () => {
const res = await fetch(`${baseUrl}/process-from-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer invalid.jwt.token",
},
body: JSON.stringify({ url: "https://example.com/test.pdf" }),
});
assert.equal(res.status, 401);
const data = await res.json();
assert.equal(data.error, "Invalid token");
});

test("rejects token signed with wrong secret", async () => {
const token = jwt.sign({ role: "authenticated" }, "wrong-secret");
const res = await fetch(`${baseUrl}/process-from-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ url: "https://example.com/test.pdf" }),
});
assert.equal(res.status, 401);
const data = await res.json();
assert.equal(data.error, "Invalid token");
});

test("accepts valid token and proceeds to route handler", async () => {
const token = jwt.sign({ role: "authenticated" }, process.env.SUPABASE_JWT_SECRET);
const res = await fetch(`${baseUrl}/process-from-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ url: "https://example.com/test.pdf" }),
});
assert.notEqual(res.status, 401, "Valid token should not be rejected by middleware");
const data = await res.json();
assert.ok(!data.error || !data.error.includes("Missing or invalid authorization token"),
"Response should not be a middleware auth error");
});

test("returns 500 when SUPABASE_JWT_SECRET is missing at request time", async () => {
const express = require("express");
const testApp = express();
testApp.use(express.json());
const testMiddleware = (req, res, next) => {
Comment on lines +798 to +802
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing or invalid authorization token" });
}
const token = authHeader.split(" ")[1];
const secret = process.env.SUPABASE_JWT_SECRET;
if (!secret) {
return res.status(500).json({ error: "Server misconfiguration: missing SUPABASE_JWT_SECRET" });
}
try {
req.user = jwt.verify(token, secret);
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
next();
};
testApp.post("/process-from-url", testMiddleware, (req, res) => {
res.json({ ok: true });
});
const testServer = http.createServer(testApp);
await new Promise((resolve) => testServer.listen(0, resolve));
const { port } = testServer.address();
const testUrl = `http://127.0.0.1:${port}`;

const savedSecret = process.env.SUPABASE_JWT_SECRET;
try {
delete process.env.SUPABASE_JWT_SECRET;
const token = jwt.sign({ role: "authenticated" }, "any-secret");
const res = await fetch(`${testUrl}/process-from-url`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ url: "https://example.com/test.pdf" }),
});
assert.equal(res.status, 500);
const data = await res.json();
assert.equal(data.error, "Server misconfiguration: missing SUPABASE_JWT_SECRET");
} finally {
process.env.SUPABASE_JWT_SECRET = savedSecret;
testServer.close();
}
});
Comment on lines +782 to +846
});
Loading