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
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
15 changes: 10 additions & 5 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<StrictMode>
<App />
</StrictMode>,
)
// Initialize CSRF token before rendering
// Use .finally() so app renders even if CSRF fetch fails
initCSRF().finally(() => {
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
})
45 changes: 44 additions & 1 deletion client/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
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) => {
Expand All @@ -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):", {
Expand Down Expand Up @@ -96,7 +120,7 @@ api.interceptors.response.use(
}
return response;
},
(error) => {
async (error) => {
if (!isDev) {
console.error("❌ API Error:", {
status: error.response?.status,
Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 75 additions & 1 deletion server/index.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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}`);
Expand All @@ -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);
Expand Down Expand Up @@ -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" });
});
Expand All @@ -167,4 +241,4 @@ server.listen(PORT, () => {
});

export default app;
export { io };
export { io };
10 changes: 10 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion server/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -676,4 +676,4 @@ router.post("/change-username", async (req, res) => {
}
});

export default router;
export default router;