Skip to content

Commit fa80676

Browse files
committed
Add CSRF protection to API endpoints and implement token management
1 parent 6e7a691 commit fa80676

File tree

2 files changed

+60
-18
lines changed

2 files changed

+60
-18
lines changed

client/src/services/api.js

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
1-
// API service for handling all fetch requests
1+
// API service for handling all fetch requests, with CSRF token support
2+
3+
let csrfToken = null;
4+
5+
// Utility to fetch and cache CSRF token
6+
async function getCsrfToken() {
7+
if (!csrfToken) {
8+
const response = await fetch('/csrf-token');
9+
const data = await response.json();
10+
csrfToken = data.csrfToken;
11+
}
12+
return csrfToken;
13+
}
14+
15+
// Utility to reset CSRF token on error (e.g. 403, token expired)
16+
function resetCsrfToken() {
17+
csrfToken = null;
18+
}
19+
20+
// Helper to include CSRF token in request headers for mutation requests
21+
async function withCsrf(headers = {}) {
22+
const token = await getCsrfToken();
23+
return { ...headers, 'csrf-token': token };
24+
}
25+
226
const API = {
3-
// Generic fetch method with error handling
4-
async fetchData(endpoint, options = {}) {
27+
// Generic fetch method with error handling and CSRF support for mutation
28+
async fetchData(endpoint, options = {}, requireCsrf = false) {
529
try {
30+
if (requireCsrf) {
31+
options.headers = await withCsrf(options.headers || {});
32+
}
633
const response = await fetch(endpoint, options);
734
if (!response.ok) {
35+
// Reset CSRF token if forbidden (possible token expiry)
36+
if (response.status === 403) resetCsrfToken();
837
const errorData = await response.json().catch(() => ({}));
938
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
1039
}
@@ -22,10 +51,10 @@ const API = {
2251
method: 'POST',
2352
headers: { 'Content-Type': 'application/json' },
2453
body: JSON.stringify(userData),
25-
}),
54+
}, true),
2655
delete: (userId) => API.fetchData(`/api/users/${userId}`, {
2756
method: 'DELETE',
28-
}),
57+
}, true),
2958
},
3059

3160
products: {
@@ -34,7 +63,7 @@ const API = {
3463
method: 'POST',
3564
headers: { 'Content-Type': 'application/json' },
3665
body: JSON.stringify(productData),
37-
}),
66+
}, true),
3867
},
3968

4069
tasks: {
@@ -43,12 +72,12 @@ const API = {
4372
method: 'POST',
4473
headers: { 'Content-Type': 'application/json' },
4574
body: JSON.stringify(taskData),
46-
}),
75+
}, true),
4776
toggleComplete: (taskId, completed) => API.fetchData(`/api/tasks/${taskId}`, {
4877
method: 'PUT',
4978
headers: { 'Content-Type': 'application/json' },
5079
body: JSON.stringify({ completed: !completed }),
51-
}),
80+
}, true),
5281
},
5382

5483
orders: {
@@ -57,7 +86,7 @@ const API = {
5786
method: 'POST',
5887
headers: { 'Content-Type': 'application/json' },
5988
body: JSON.stringify(orderData),
60-
}),
89+
}, true),
6190
},
6291

6392
search: {

server/index.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const express = require('express');
22
const cors = require('cors');
3+
const cookieParser = require('cookie-parser');
4+
const csurf = require('csurf');
35
require('dotenv').config();
46

57
const app = express();
@@ -29,7 +31,18 @@ let tasks = [
2931

3032
// Middleware
3133
app.use(cors());
34+
app.use(cookieParser());
3235
app.use(express.json());
36+
app.use(express.urlencoded({ extended: true }));
37+
38+
// CSRF protection middleware
39+
const csrfProtection = csurf({ cookie: true });
40+
app.use(csrfProtection);
41+
42+
// Endpoint to provide CSRF token to clients
43+
app.get('/csrf-token', (req, res) => {
44+
res.json({ csrfToken: req.csrfToken() });
45+
});
3346

3447
// Request logging middleware
3548
app.use((req, res, next) => {
@@ -107,7 +120,7 @@ app.get('/api/users/:id', (req, res) => {
107120
res.json(user);
108121
});
109122

110-
app.post('/api/users', (req, res) => {
123+
app.post('/api/users', csrfProtection, (req, res) => {
111124
const { name, email, role = 'user' } = req.body;
112125

113126
if (!name || !email) {
@@ -136,7 +149,7 @@ app.post('/api/users', (req, res) => {
136149
});
137150
});
138151

139-
app.put('/api/users/:id', (req, res) => {
152+
app.put('/api/users/:id', csrfProtection, (req, res) => {
140153
const userId = parseInt(req.params.id);
141154
const userIndex = users.findIndex(u => u.id === userId);
142155

@@ -153,7 +166,7 @@ app.put('/api/users/:id', (req, res) => {
153166
});
154167
});
155168

156-
app.delete('/api/users/:id', (req, res) => {
169+
app.delete('/api/users/:id', csrfProtection, (req, res) => {
157170
const userId = parseInt(req.params.id);
158171
const userIndex = users.findIndex(u => u.id === userId);
159172

@@ -217,7 +230,7 @@ app.get('/api/products/:id', (req, res) => {
217230
res.json(product);
218231
});
219232

220-
app.post('/api/products', (req, res) => {
233+
app.post('/api/products', csrfProtection, (req, res) => {
221234
const { name, price, category, stock, description } = req.body;
222235

223236
if (!name || !price || !category) {
@@ -261,7 +274,7 @@ app.get('/api/orders', (req, res) => {
261274
});
262275
});
263276

264-
app.post('/api/orders', (req, res) => {
277+
app.post('/api/orders', csrfProtection, (req, res) => {
265278
const { userId, productId, quantity = 1 } = req.body;
266279

267280
if (!userId || !productId) {
@@ -331,7 +344,7 @@ app.get('/api/tasks', (req, res) => {
331344
});
332345
});
333346

334-
app.post('/api/tasks', (req, res) => {
347+
app.post('/api/tasks', csrfProtection, (req, res) => {
335348
const { title, priority = 'medium', assignedTo } = req.body;
336349

337350
if (!title) {
@@ -355,7 +368,7 @@ app.post('/api/tasks', (req, res) => {
355368
});
356369
});
357370

358-
app.put('/api/tasks/:id', (req, res) => {
371+
app.put('/api/tasks/:id', csrfProtection, (req, res) => {
359372
const taskId = parseInt(req.params.id);
360373
const taskIndex = tasks.findIndex(t => t.id === taskId);
361374

@@ -508,7 +521,7 @@ app.get('/api/test/large-data', (req, res) => {
508521
});
509522
});
510523

511-
app.post('/api/test/echo', (req, res) => {
524+
app.post('/api/test/echo', csrfProtection, (req, res) => {
512525
res.json({
513526
message: 'Echo endpoint - returning your data',
514527
received: req.body,
@@ -575,7 +588,7 @@ app.get('/api/users/paginated', (req, res) => {
575588
});
576589

577590
// ============ BULK OPERATIONS ============
578-
app.post('/api/users/bulk', (req, res) => {
591+
app.post('/api/users/bulk', csrfProtection, (req, res) => {
579592
const { users: newUsers } = req.body;
580593

581594
if (!Array.isArray(newUsers)) {

0 commit comments

Comments
 (0)