@startsimpli/auth 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -85,18 +85,15 @@ function makeJwt(payload: object): string {
|
|
|
85
85
|
|
|
86
86
|
const VALID_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 3600, user_id: '1' });
|
|
87
87
|
|
|
88
|
-
describe('CSRF
|
|
88
|
+
describe('CSRF not required for signin/register (endpoints are @csrf_exempt)', () => {
|
|
89
89
|
beforeEach(() => {
|
|
90
90
|
vi.clearAllMocks();
|
|
91
91
|
mockSessionStorage.clear();
|
|
92
92
|
mockLocalStorage.clear();
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
it('signInWithCredentials
|
|
96
|
-
mockFetch.mockImplementation((
|
|
97
|
-
if (url.includes('/csrf/')) {
|
|
98
|
-
return Promise.resolve({ ok: true });
|
|
99
|
-
}
|
|
95
|
+
it('signInWithCredentials does not fetch or send CSRF token', async () => {
|
|
96
|
+
mockFetch.mockImplementation(() => {
|
|
100
97
|
return Promise.resolve({
|
|
101
98
|
ok: true,
|
|
102
99
|
status: 200,
|
|
@@ -106,20 +103,23 @@ describe('CSRF token on signin/register', () => {
|
|
|
106
103
|
|
|
107
104
|
await signInWithCredentials('test@test.com', 'password');
|
|
108
105
|
|
|
109
|
-
//
|
|
106
|
+
// Should NOT call the CSRF endpoint
|
|
107
|
+
const csrfCall = mockFetch.mock.calls.find(
|
|
108
|
+
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/csrf/')
|
|
109
|
+
);
|
|
110
|
+
expect(csrfCall).toBeUndefined();
|
|
111
|
+
|
|
112
|
+
// Token endpoint call should not include X-CSRFToken header
|
|
110
113
|
const tokenCall = mockFetch.mock.calls.find(
|
|
111
114
|
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/auth/token/') && !(c[0] as string).includes('csrf') && !(c[0] as string).includes('refresh')
|
|
112
115
|
);
|
|
113
116
|
expect(tokenCall).toBeDefined();
|
|
114
117
|
const headers = tokenCall![1]?.headers;
|
|
115
|
-
expect(headers['X-CSRFToken']).
|
|
118
|
+
expect(headers['X-CSRFToken']).toBeUndefined();
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
it('registerAccount
|
|
119
|
-
mockFetch.mockImplementation((
|
|
120
|
-
if (url.includes('/csrf/')) {
|
|
121
|
-
return Promise.resolve({ ok: true });
|
|
122
|
-
}
|
|
121
|
+
it('registerAccount does not fetch or send CSRF token', async () => {
|
|
122
|
+
mockFetch.mockImplementation(() => {
|
|
123
123
|
return Promise.resolve({
|
|
124
124
|
ok: true,
|
|
125
125
|
status: 200,
|
|
@@ -133,12 +133,18 @@ describe('CSRF token on signin/register', () => {
|
|
|
133
133
|
passwordConfirm: 'securepassword',
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
// Should NOT call the CSRF endpoint
|
|
137
|
+
const csrfCall = mockFetch.mock.calls.find(
|
|
138
|
+
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/csrf/')
|
|
139
|
+
);
|
|
140
|
+
expect(csrfCall).toBeUndefined();
|
|
141
|
+
|
|
136
142
|
const registerCall = mockFetch.mock.calls.find(
|
|
137
143
|
(c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/auth/register/')
|
|
138
144
|
);
|
|
139
145
|
expect(registerCall).toBeDefined();
|
|
140
146
|
const headers = registerCall![1]?.headers;
|
|
141
|
-
expect(headers['X-CSRFToken']).
|
|
147
|
+
expect(headers['X-CSRFToken']).toBeUndefined();
|
|
142
148
|
});
|
|
143
149
|
});
|
|
144
150
|
|
package/src/client/functions.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface AuthUser {
|
|
|
20
20
|
groups?: string[];
|
|
21
21
|
permissions?: string[];
|
|
22
22
|
isActive?: boolean;
|
|
23
|
+
isStaff?: boolean;
|
|
23
24
|
isEmailVerified?: boolean;
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -136,6 +137,9 @@ export function setAccessToken(token: string | null): void {
|
|
|
136
137
|
// Also clear from the other storage in case rememberMe was toggled
|
|
137
138
|
if (_storageAvailable('sessionStorage')) sessionStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
138
139
|
if (_storageAvailable('localStorage')) localStorage.removeItem(TOKEN_STORAGE_KEY);
|
|
140
|
+
// Clear ALL auth cookies so middleware doesn't redirect back
|
|
141
|
+
// (prevents infinite loop when auth state is corrupted)
|
|
142
|
+
_clearAllAuthCookies();
|
|
139
143
|
} else {
|
|
140
144
|
storage.setItem(TOKEN_STORAGE_KEY, token);
|
|
141
145
|
}
|
|
@@ -145,6 +149,14 @@ export function setAccessToken(token: string | null): void {
|
|
|
145
149
|
_memToken = token;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
/** Clear every cookie the middleware checks so it won't redirect back to the app. */
|
|
153
|
+
function _clearAllAuthCookies(): void {
|
|
154
|
+
if (typeof document === 'undefined') return;
|
|
155
|
+
for (const name of ['auth_session', 'access_token', 'refresh_token']) {
|
|
156
|
+
document.cookie = `${name}=; path=/; max-age=0`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
148
160
|
const AUTH_COOKIE_NAME = 'auth_session';
|
|
149
161
|
|
|
150
162
|
/** Derive cookie max-age from JWT exp claim instead of hardcoding. */
|
|
@@ -213,6 +225,7 @@ function normalizeUser(raw: unknown): AuthUser | null {
|
|
|
213
225
|
groups: Array.isArray(payload.groups) ? (payload.groups as string[]) : [],
|
|
214
226
|
permissions: Array.isArray(payload.permissions) ? (payload.permissions as string[]) : [],
|
|
215
227
|
isActive: (payload.isActive ?? payload.is_active) as boolean | undefined,
|
|
228
|
+
isStaff: (payload.isStaff ?? payload.is_staff) as boolean | undefined,
|
|
216
229
|
isEmailVerified: (payload.isEmailVerified ?? payload.is_email_verified) as boolean | undefined,
|
|
217
230
|
};
|
|
218
231
|
}
|
|
@@ -230,13 +243,11 @@ function parseAuthResponse(data: unknown): { access?: string; user?: AuthUser }
|
|
|
230
243
|
// --- Auth functions ---
|
|
231
244
|
|
|
232
245
|
export async function signInWithCredentials(email: string, password: string) {
|
|
233
|
-
|
|
234
|
-
const csrfToken = getCsrfToken();
|
|
246
|
+
// No CSRF needed — Django's /auth/token/ is @csrf_exempt (JWT endpoint)
|
|
235
247
|
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN), {
|
|
236
248
|
method: 'POST',
|
|
237
249
|
headers: {
|
|
238
250
|
'Content-Type': 'application/json',
|
|
239
|
-
...(csrfToken ? { 'X-CSRFToken': csrfToken } : {}),
|
|
240
251
|
},
|
|
241
252
|
credentials: 'include',
|
|
242
253
|
body: JSON.stringify({ email, password }),
|
|
@@ -273,13 +284,11 @@ export async function registerAccount(payload: {
|
|
|
273
284
|
const [firstFromName, ...rest] = rawName ? rawName.split(/\s+/) : [];
|
|
274
285
|
const lastFromName = rest.length ? rest.join(' ') : undefined;
|
|
275
286
|
|
|
276
|
-
|
|
277
|
-
const csrfToken = getCsrfToken();
|
|
287
|
+
// No CSRF needed — Django's /auth/register/ is @csrf_exempt
|
|
278
288
|
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.REGISTER), {
|
|
279
289
|
method: 'POST',
|
|
280
290
|
headers: {
|
|
281
291
|
'Content-Type': 'application/json',
|
|
282
|
-
...(csrfToken ? { 'X-CSRFToken': csrfToken } : {}),
|
|
283
292
|
},
|
|
284
293
|
credentials: 'include',
|
|
285
294
|
body: JSON.stringify({
|