refactor(auth): extract extractFaSessionId to \$lib/shared/cookies
Move the Set-Cookie parser out of login/+page.server.ts into a shared module with its own Vitest coverage (single-header, multi-header getSetCookie path, missing-header, attribute-stripping, prefix-match-rejection). An Undici or Node upgrade that changes header shape now trips its own test instead of silently breaking login. Addresses PR #612 / Felix F2. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
38
frontend/src/lib/shared/cookies.spec.ts
Normal file
38
frontend/src/lib/shared/cookies.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { extractFaSessionId } from './cookies';
|
||||
|
||||
describe('extractFaSessionId', () => {
|
||||
it('extracts the opaque id from a single Set-Cookie header', () => {
|
||||
const headers = ['fa_session=abc123; Path=/; HttpOnly; SameSite=Strict'];
|
||||
expect(extractFaSessionId(headers)).toBe('abc123');
|
||||
});
|
||||
|
||||
it('extracts the value when multiple Set-Cookie headers are present (getSetCookie path)', () => {
|
||||
const headers = [
|
||||
'JSESSIONID=legacy; Path=/',
|
||||
'fa_session=xyz789; Path=/; Max-Age=28800; HttpOnly',
|
||||
'XSRF-TOKEN=ignored; Path=/'
|
||||
];
|
||||
expect(extractFaSessionId(headers)).toBe('xyz789');
|
||||
});
|
||||
|
||||
it('returns null when no header carries fa_session', () => {
|
||||
expect(extractFaSessionId(['Other=foo; Path=/'])).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for an empty header list', () => {
|
||||
expect(extractFaSessionId([])).toBeNull();
|
||||
});
|
||||
|
||||
it('strips all attributes after the first semicolon', () => {
|
||||
const headers = ['fa_session=opaque-token-with.dots_and-dashes; Path=/; Secure; HttpOnly'];
|
||||
expect(extractFaSessionId(headers)).toBe('opaque-token-with.dots_and-dashes');
|
||||
});
|
||||
|
||||
it('only matches a cookie whose name is exactly fa_session', () => {
|
||||
// A different cookie name that happens to contain "fa_session" as a substring
|
||||
// must not match — anchored to start of header.
|
||||
const headers = ['xfa_session=should-not-match; Path=/'];
|
||||
expect(extractFaSessionId(headers)).toBeNull();
|
||||
});
|
||||
});
|
||||
20
frontend/src/lib/shared/cookies.ts
Normal file
20
frontend/src/lib/shared/cookies.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Extracts the fa_session cookie value from a list of Set-Cookie response headers.
|
||||
*
|
||||
* The backend may append attributes like `Path`, `HttpOnly`, `SameSite=Strict`,
|
||||
* `Max-Age`, `Secure`; we only forward the opaque session id — the SvelteKit
|
||||
* cookies API rewrites the attributes itself when re-emitting to the browser.
|
||||
*
|
||||
* Pass the result of `response.headers.getSetCookie()` (modern Node/Undici) or
|
||||
* a single-element array containing `response.headers.get('set-cookie')` for
|
||||
* older runtimes that lack `getSetCookie`.
|
||||
*
|
||||
* Returns `null` if no fa_session cookie is present.
|
||||
*/
|
||||
export function extractFaSessionId(setCookieHeaders: string[]): string | null {
|
||||
for (const header of setCookieHeaders) {
|
||||
const match = header.match(/^fa_session=([^;]+)/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user