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