Skip to content

Commit 8cc130d

Browse files
committed
dash csrf fix
1 parent 50a7cdb commit 8cc130d

File tree

4 files changed

+48
-6
lines changed

4 files changed

+48
-6
lines changed

frontend/src/lib/api.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ const API_BASE = isLocalhost()
1616
? 'http://localhost:8787' // Local development
1717
: 'https://commentkit.ankushkun.workers.dev'; // Production
1818

19+
// CSRF token storage
20+
let csrfToken: string | null = null;
21+
22+
export function setCsrfToken(token: string | null) {
23+
csrfToken = token;
24+
}
25+
26+
export function getCsrfToken(): string | null {
27+
return csrfToken;
28+
}
29+
1930
interface ApiResponse<T> {
2031
data: T | null;
2132
error: string | null;
@@ -27,13 +38,21 @@ async function request<T>(
2738
options: RequestInit = {}
2839
): Promise<ApiResponse<T>> {
2940
try {
41+
const headers: Record<string, string> = {
42+
'Content-Type': 'application/json',
43+
...(options.headers as Record<string, string>),
44+
};
45+
46+
// Add CSRF token for mutation requests
47+
const method = options.method?.toUpperCase();
48+
if (csrfToken && method && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
49+
headers['X-CSRF-Token'] = csrfToken;
50+
}
51+
3052
const res = await fetch(`${API_BASE}${path}`, {
3153
...options,
3254
credentials: 'include', // Include HttpOnly cookies for authentication
33-
headers: {
34-
'Content-Type': 'application/json',
35-
...options.headers,
36-
},
55+
headers,
3756
});
3857

3958
const json = await res.json().catch(() => null);
@@ -75,7 +94,7 @@ export const auth = {
7594
}),
7695

7796
verify: (token: string) =>
78-
request<{ token: string; user: User }>(`/api/v1/auth/verify?token=${token}`),
97+
request<{ token: string; user: User; csrf_token: string }>(`/api/v1/auth/verify?token=${token}`),
7998

8099
// Get current user, optionally with bootstrap data to save an extra API call
81100
me: (options?: { bootstrap?: boolean }) =>
@@ -284,6 +303,7 @@ export interface User {
284303
is_superadmin: boolean;
285304
created_at: string;
286305
updated_at: string;
306+
csrf_token?: string;
287307
}
288308

289309
export interface GlobalStats {

frontend/src/lib/auth-context.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, useContext, useEffect, useState, useRef, type ReactNode } from 'react';
2-
import { auth, type User, type BootstrapData } from './api';
2+
import { auth, type User, type BootstrapData, setCsrfToken } from './api';
33

44
interface AuthContextType {
55
user: User | null;
@@ -27,7 +27,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
2727
if (error) {
2828
// User not authenticated (no valid cookie)
2929
setUser(null);
30+
setCsrfToken(null);
3031
} else if (data) {
32+
// Store CSRF token for future requests
33+
if (data.csrf_token) {
34+
setCsrfToken(data.csrf_token);
35+
}
3136
// Store bootstrap data for consumption
3237
if (data.bootstrap) {
3338
bootstrapRef.current = data.bootstrap;
@@ -49,6 +54,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
4954
// Verify token - server will set HttpOnly cookie in response
5055
auth.verify(token).then(({ data, error }) => {
5156
if (data && !error) {
57+
// Store CSRF token
58+
if (data.csrf_token) {
59+
setCsrfToken(data.csrf_token);
60+
}
5261
// No need to store token - server sets HttpOnly cookie
5362
setUser(data.user);
5463

@@ -80,6 +89,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
8089
// Server will clear HttpOnly cookie
8190
await auth.logout();
8291
setUser(null);
92+
setCsrfToken(null);
8393
bootstrapRef.current = null;
8494
};
8595

worker/src/middleware/csrf.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export async function validateCsrf(c: Context<{ Bindings: Env }>, next: Function
138138
const exemptPaths = [
139139
'/api/v1/auth/login', // Entry point - user doesn't have a token yet
140140
'/api/v1/auth/verify', // Uses magic link token, not session-based
141+
'/api/v1/auth/logout', // Allow logout even if CSRF token expired
141142
];
142143

143144
if (exemptPaths.includes(path)) {

worker/src/routes/auth.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { zValidator } from '@hono/zod-validator';
33
import { z } from 'zod';
44
import { Database } from '../db';
55
import { getAuthUser, hashToken } from '../middleware';
6+
import { generateCsrfToken } from '../middleware/csrf';
67
import type { Env } from '../types';
78
import { sendMagicLinkEmail } from '../utils/email';
89

@@ -160,9 +161,14 @@ auth.get('/verify', async (c) => {
160161
// Set HttpOnly cookie for the session
161162
const cookie = createAuthCookie(sessionToken, c.env, 30);
162163

164+
// Generate CSRF token for the frontend
165+
const origin = c.req.header('Origin') || c.env.FRONTEND_URL || c.env.BASE_URL || '';
166+
const csrfToken = await generateCsrfToken(origin, c.env.JWT_SECRET);
167+
163168
// Return response with cookie
164169
const response = c.json({
165170
token: sessionToken, // Still return token for backward compatibility
171+
csrf_token: csrfToken,
166172
user: {
167173
id: user.id,
168174
email: user.email,
@@ -187,6 +193,10 @@ auth.get('/me', async (c) => {
187193

188194
const includeBootstrap = c.req.query('bootstrap') === 'true';
189195

196+
// Generate CSRF token for the frontend
197+
const origin = c.req.header('Origin') || c.env.FRONTEND_URL || c.env.BASE_URL || '';
198+
const csrfToken = await generateCsrfToken(origin, c.env.JWT_SECRET);
199+
190200
const userData = {
191201
id: user.id,
192202
email: user.email,
@@ -195,6 +205,7 @@ auth.get('/me', async (c) => {
195205
is_superadmin: user.is_superadmin,
196206
created_at: user.created_at,
197207
updated_at: user.updated_at,
208+
csrf_token: csrfToken,
198209
};
199210

200211
if (!includeBootstrap) {

0 commit comments

Comments
 (0)