Skip to content

Commit dc8f708

Browse files
committed
csrf fix: local storage -> session storage
1 parent 053b4b5 commit dc8f708

File tree

2 files changed

+18
-41
lines changed

2 files changed

+18
-41
lines changed

frontend/src/stores/__tests__/auth.test.ts

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ vi.mock('../../lib/api', () => ({
1616
}));
1717

1818
describe('auth store', () => {
19-
let localStorageData: Record<string, string> = {};
19+
let sessionStorageData: Record<string, string> = {};
2020

2121
beforeEach(async () => {
22-
// Reset localStorage mock
23-
localStorageData = {};
24-
vi.mocked(localStorage.getItem).mockImplementation((key: string) => localStorageData[key] ?? null);
25-
vi.mocked(localStorage.setItem).mockImplementation((key: string, value: string) => {
26-
localStorageData[key] = value;
22+
// Reset sessionStorage mock
23+
sessionStorageData = {};
24+
vi.mocked(sessionStorage.getItem).mockImplementation((key: string) => sessionStorageData[key] ?? null);
25+
vi.mocked(sessionStorage.setItem).mockImplementation((key: string, value: string) => {
26+
sessionStorageData[key] = value;
2727
});
28-
vi.mocked(localStorage.removeItem).mockImplementation((key: string) => {
29-
delete localStorageData[key];
28+
vi.mocked(sessionStorage.removeItem).mockImplementation((key: string) => {
29+
delete sessionStorageData[key];
3030
});
3131

3232
// Reset all mocks
@@ -59,7 +59,7 @@ describe('auth store', () => {
5959
expect(get(userRole)).toBe(null);
6060
});
6161

62-
it('restores auth state from localStorage', async () => {
62+
it('restores auth state from sessionStorage', async () => {
6363
const authState = {
6464
isAuthenticated: true,
6565
username: 'testuser',
@@ -69,7 +69,7 @@ describe('auth store', () => {
6969
userEmail: '[email protected]',
7070
timestamp: Date.now(),
7171
};
72-
localStorageData['authState'] = JSON.stringify(authState);
72+
sessionStorageData['authState'] = JSON.stringify(authState);
7373

7474
const { isAuthenticated, username, userRole, csrfToken } = await import('../auth');
7575

@@ -78,24 +78,6 @@ describe('auth store', () => {
7878
expect(get(userRole)).toBe('user');
7979
expect(get(csrfToken)).toBe('test-token');
8080
});
81-
82-
it('clears expired auth state from localStorage', async () => {
83-
const authState = {
84-
isAuthenticated: true,
85-
username: 'testuser',
86-
userRole: 'user',
87-
csrfToken: 'test-token',
88-
userId: 'user-123',
89-
userEmail: '[email protected]',
90-
timestamp: Date.now() - 25 * 60 * 60 * 1000, // 25 hours ago
91-
};
92-
localStorageData['authState'] = JSON.stringify(authState);
93-
94-
const { isAuthenticated } = await import('../auth');
95-
96-
expect(get(isAuthenticated)).toBe(null);
97-
expect(localStorage.removeItem).toHaveBeenCalledWith('authState');
98-
});
9981
});
10082

10183
describe('login', () => {
@@ -124,7 +106,7 @@ describe('auth store', () => {
124106
expect(get(csrfToken)).toBe('new-csrf-token');
125107
});
126108

127-
it('persists auth state to localStorage on login', async () => {
109+
it('persists auth state to sessionStorage on login', async () => {
128110
mockLoginApi.mockResolvedValue({
129111
data: {
130112
username: 'testuser',
@@ -141,7 +123,7 @@ describe('auth store', () => {
141123
const { login } = await import('../auth');
142124
await login('testuser', 'password123');
143125

144-
expect(localStorage.setItem).toHaveBeenCalledWith(
126+
expect(sessionStorage.setItem).toHaveBeenCalledWith(
145127
'authState',
146128
expect.stringContaining('testuser')
147129
);
@@ -201,13 +183,13 @@ describe('auth store', () => {
201183
expect(get(username)).toBe(null);
202184
});
203185

204-
it('clears localStorage on logout', async () => {
186+
it('clears sessionStorage on logout', async () => {
205187
mockLogoutApi.mockResolvedValue({ data: {}, error: null });
206188

207189
const { logout } = await import('../auth');
208190
await logout();
209191

210-
expect(localStorage.removeItem).toHaveBeenCalledWith('authState');
192+
expect(sessionStorage.removeItem).toHaveBeenCalledWith('authState');
211193
});
212194

213195
it('still clears state even if API call fails', async () => {

frontend/src/stores/auth.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,19 @@ interface AuthState {
1919
function getPersistedAuthState(): AuthState | null {
2020
if (typeof window === 'undefined') return null;
2121
try {
22-
const data = localStorage.getItem('authState');
22+
const data = sessionStorage.getItem('authState');
2323
if (!data) return null;
24-
const parsed = JSON.parse(data) as AuthState;
25-
if (Date.now() - parsed.timestamp > 24 * 60 * 60 * 1000) {
26-
localStorage.removeItem('authState');
27-
return null;
28-
}
29-
return parsed;
24+
return JSON.parse(data) as AuthState;
3025
} catch { return null; }
3126
}
3227

3328
function persistAuthState(state: Partial<AuthState> | null) {
3429
if (typeof window === 'undefined') return;
3530
if (!state || state.isAuthenticated === false) {
36-
localStorage.removeItem('authState');
31+
sessionStorage.removeItem('authState');
3732
return;
3833
}
39-
localStorage.setItem('authState', JSON.stringify({ ...state, timestamp: Date.now() }));
34+
sessionStorage.setItem('authState', JSON.stringify({ ...state, timestamp: Date.now() }));
4035
}
4136

4237
const persisted = getPersistedAuthState();

0 commit comments

Comments
 (0)