diff --git a/Readme.md b/Readme.md
index bd842e8..addbf20 100644
--- a/Readme.md
+++ b/Readme.md
@@ -224,6 +224,7 @@ Key environment variables needed:
| ------------------------- | ------------------------- |
| `DATABASE_URL` | MongoDB connection string |
| `JWT_SECRET` | JWT signing secret |
+| `CSRF_SECRET` | CSRF token signing secret |
| `GROQ_API_KEY` | Groq API for AI features |
| `OPENAI_API_KEY` | OpenAI for embeddings |
| `PINECONE_API_KEY` | Pinecone vector database |
diff --git a/client/src/main.tsx b/client/src/main.tsx
index bef5202..0fa2e8d 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -2,9 +2,14 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
+import { initCSRF } from './utils/api'
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
+// Initialize CSRF token before rendering
+// Use .finally() so app renders even if CSRF fetch fails
+initCSRF().finally(() => {
+ createRoot(document.getElementById('root')!).render(
+
+
+ ,
+ )
+})
diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts
index c87720e..6560302 100644
--- a/client/src/utils/api.ts
+++ b/client/src/utils/api.ts
@@ -44,8 +44,23 @@ const api = axios.create({
headers: {
"Content-Type": "application/json",
},
+ withCredentials: true, // Enable cookies for CSRF
});
+// Store CSRF token
+let csrfToken: string | null = null;
+
+// Fetch CSRF token from server
+export const initCSRF = async (): Promise => {
+ try {
+ const response = await api.get("/csrf-token");
+ csrfToken = response.data.csrfToken;
+ console.log("✅ CSRF token initialized");
+ } catch (error) {
+ console.error("❌ Failed to fetch CSRF token:", error);
+ }
+};
+
// Request interceptor for debugging
api.interceptors.request.use(
(config) => {
@@ -55,6 +70,15 @@ api.interceptors.request.use(
config.headers.Authorization = `Bearer ${token}`;
}
+ // Add CSRF token for state-changing requests
+ if (
+ csrfToken &&
+ config.method &&
+ ["post", "put", "delete", "patch"].includes(config.method.toLowerCase())
+ ) {
+ config.headers["x-csrf-token"] = csrfToken;
+ }
+
// Debug logging for courses API
if (config.url?.includes("/courses")) {
console.log("🚀 API Request (Courses):", {
@@ -96,7 +120,7 @@ api.interceptors.response.use(
}
return response;
},
- (error) => {
+ async (error) => {
if (!isDev) {
console.error("❌ API Error:", {
status: error.response?.status,
@@ -107,6 +131,25 @@ api.interceptors.response.use(
});
}
+ // Handle CSRF token errors
+ if (
+ error.response?.status === 403 &&
+ error.response?.data?.message?.includes("CSRF") &&
+ !error.config._retry
+ ) {
+ error.config._retry = true;
+
+ // Refresh CSRF token
+ await initCSRF();
+
+ // Retry the original request
+ if (csrfToken) {
+ error.config.headers["x-csrf-token"] = csrfToken;
+ }
+
+ return api(error.config);
+ }
+
if (error.response?.status === 401) {
console.log("🔐 401 Unauthorized - Clearing auth state");
removeAuthToken();
diff --git a/server/.env.example b/server/.env.example
index 0fb26ac..151212f 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -4,6 +4,9 @@ DATABASE_URL=your_mongodb_connection_string
# JWT
JWT_SECRET=your_super_secret_jwt_key_here
+# CSRF Protection
+CSRF_SECRET=your_csrf_secret_key_minimum_32_characters_long
+
# Email Configuration (Gmail)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
diff --git a/server/index.js b/server/index.js
index f5cc645..4281d4b 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,6 +1,7 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
+import { doubleCsrf } from "csrf-csrf";
import dotenv from "dotenv";
import { createServer } from "http";
import { Server } from "socket.io";
@@ -38,6 +39,32 @@ initRedis().catch((err) => {
const app = express();
const server = createServer(app);
+// CSRF Configuration
+const csrfProtection = doubleCsrf({
+ getSecret: () => {
+ if (!process.env.CSRF_SECRET) {
+ throw new Error("CSRF_SECRET environment variable is not configured. Please set it in your .env file.");
+ }
+ return process.env.CSRF_SECRET;
+ },
+ cookieName: "x-csrf-token",
+ cookieOptions: {
+ httpOnly: true,
+ sameSite: process.env.NODE_ENV === "production" ? "strict" : "lax",
+ secure: process.env.NODE_ENV === "production",
+ maxAge: 3600000, // 1 hour
+ },
+ size: 64,
+ ignoredMethods: ["GET", "HEAD", "OPTIONS"],
+ getSessionIdentifier: (req) => {
+ // Use session ID or user ID if available, otherwise use a default
+ return req.session?.id || req.user?.id || "anonymous";
+ },
+});
+
+const generateToken = csrfProtection.generateCsrfToken; // Correct function name
+const doubleCsrfProtection = csrfProtection.doubleCsrfProtection;
+
// Determine allowed origins based on environment
const getAllowedOrigins = () => {
const origins = [];
@@ -78,6 +105,17 @@ app.use(express.urlencoded({ extended: true, limit: "50mb" }));
// Initialize Passport
app.use(passport.initialize());
+// CSRF token endpoint - clients fetch token from here
+app.get("/api/csrf-token", (req, res) => {
+ try {
+ const token = generateToken(req, res);
+ res.json({ csrfToken: token });
+ } catch (error) {
+ console.error("CSRF token generation error:", error);
+ res.status(500).json({ error: "Failed to generate CSRF token" });
+ }
+});
+
// Debug middleware to log all requests
app.use((req, res, next) => {
console.log(`🔍 ${req.method} ${req.path}`);
@@ -90,6 +128,33 @@ app.use((req, res, next) => {
next();
});
+// Apply CSRF protection to all API routes (except specific public endpoints)
+app.use("/api", (req, res, next) => {
+ // Skip CSRF validation for:
+ // 1. Safe methods (GET, HEAD, OPTIONS - already ignored by default)
+ // 2. CSRF token endpoint itself
+ // 3. Public authentication endpoints
+ const publicPaths = [
+ "/csrf-token",
+ "/auth/login",
+ "/auth/signup",
+ "/auth/send-otp",
+ "/auth/verify-otp",
+ "/auth/forgot-password",
+ "/auth/reset-password",
+ "/auth/google",
+ "/auth/google/callback",
+ "/health",
+ ];
+
+ if (publicPaths.some((path) => req.path === path)) {
+ return next();
+ }
+
+ // Apply CSRF protection
+ doubleCsrfProtection(req, res, next);
+});
+
// Routes
app.use("/api/auth", authRoutes);
app.use("/api/pdfs", pdfRoutes);
@@ -147,6 +212,15 @@ app.get("/api/health", async (req, res) => {
// Error handling middleware
app.use((err, req, res, next) => {
+ // Handle CSRF token errors
+ if (err.code === "EBADCSRFTOKEN") {
+ console.log("❌ CSRF validation failed:", req.method, req.path);
+ return res.status(403).json({
+ error: "Invalid or missing CSRF token",
+ code: "EBADCSRFTOKEN",
+ });
+ }
+
console.error("Unhandled error:", err);
res.status(500).json({ error: "Internal server error" });
});
@@ -167,4 +241,4 @@ server.listen(PORT, () => {
});
export default app;
-export { io };
+export { io };
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
index a9c4faf..d64951d 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -16,6 +16,7 @@
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
+ "csrf-csrf": "^4.0.3",
"dotenv": "^17.2.1",
"express": "^4.18.2",
"groq-sdk": "^0.30.0",
@@ -2451,6 +2452,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/csrf-csrf": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-4.0.3.tgz",
+ "integrity": "sha512-DaygOzelL4Qo1pHwI9LPyZL+X2456/OzpT596kNeZGiTSqKVDOk/9PPJ+FjzZacjMUEusOHw3WJKe1RW4iUhrw==",
+ "license": "ISC",
+ "dependencies": {
+ "http-errors": "^2.0.0"
+ }
+ },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
diff --git a/server/package.json b/server/package.json
index df81ad1..1fb2e85 100644
--- a/server/package.json
+++ b/server/package.json
@@ -20,6 +20,7 @@
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
+ "csrf-csrf": "^4.0.3",
"dotenv": "^17.2.1",
"express": "^4.18.2",
"groq-sdk": "^0.30.0",
diff --git a/server/routes/auth.js b/server/routes/auth.js
index 6eb2a55..a874380 100644
--- a/server/routes/auth.js
+++ b/server/routes/auth.js
@@ -676,4 +676,4 @@ router.post("/change-username", async (req, res) => {
}
});
-export default router;
+export default router;
\ No newline at end of file