feat(auth): hooks forward fa_session cookie instead of injecting Basic auth
userGroup: GET /api/users/me with Cookie: fa_session=<id>. On 401, drop the stale cookie and redirect to /login?reason=expired (unless already on a public path) so the user sees an explainer instead of a silent kick. handleFetch: forward fa_session as a Cookie header on every API call except the public auth endpoints. Drops the old auth_token injection. Also adds a one-off cleanup of any lingering auth_token cookie from pre-migration sessions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,21 +58,40 @@ const handleParaglide: Handle = ({ event, resolve }) =>
|
||||
});
|
||||
|
||||
const userGroup: Handle = async ({ event, resolve }) => {
|
||||
const auth = event.cookies.get('auth_token');
|
||||
// One-off cleanup of the legacy Basic-credentials cookie from before the Spring Session migration (#523).
|
||||
if (event.cookies.get('auth_token')) {
|
||||
event.cookies.delete('auth_token', { path: '/' });
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
try {
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const response = await fetch(`${apiUrl}/api/users/me`, {
|
||||
headers: { Authorization: auth }
|
||||
});
|
||||
if (response.ok) {
|
||||
const user = await response.json();
|
||||
event.locals.user = user;
|
||||
const sessionId = event.cookies.get('fa_session');
|
||||
if (!sessionId) {
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const response = await fetch(`${apiUrl}/api/users/me`, {
|
||||
headers: { Cookie: `fa_session=${sessionId}` }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
event.locals.user = await response.json();
|
||||
} else if (response.status === 401) {
|
||||
// Backend rejected the session (expired or invalidated). Drop the stale
|
||||
// cookie and surface the reason on the login page. PUBLIC_PATHS check
|
||||
// avoids a redirect loop if the user is already on /login.
|
||||
event.cookies.delete('fa_session', { path: '/' });
|
||||
const isPublic = PUBLIC_PATHS.some((p) => event.url.pathname.startsWith(p));
|
||||
if (!isPublic) {
|
||||
throw redirect(302, '/login?reason=expired');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching user in hook:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
// Don't swallow SvelteKit redirects — they're thrown as objects with a `status` field.
|
||||
if (error instanceof Object && 'status' in error && 'location' in error) {
|
||||
throw error;
|
||||
}
|
||||
console.error('Error fetching user in hook:', error);
|
||||
}
|
||||
|
||||
return resolve(event);
|
||||
@@ -83,14 +102,11 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
|
||||
const isApi = request.url.startsWith(apiUrl) || request.url.includes('/api/');
|
||||
|
||||
if (isApi) {
|
||||
// If the request already carries an explicit Authorization header (e.g. the
|
||||
// login action sends Basic auth), pass it through unchanged.
|
||||
if (request.headers.has('Authorization')) {
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
// Password reset endpoints are public — no auth header needed.
|
||||
// Auth endpoints that establish/check their own credentials manage cookies themselves;
|
||||
// don't double-inject a stale fa_session.
|
||||
const PUBLIC_API_PATHS = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/logout',
|
||||
'/api/auth/forgot-password',
|
||||
'/api/auth/reset-password',
|
||||
'/api/auth/invite/',
|
||||
@@ -100,24 +116,20 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
const token = event.cookies.get('auth_token');
|
||||
|
||||
if (!token) {
|
||||
const sessionId = event.cookies.get('fa_session');
|
||||
if (!sessionId) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
// Clone the request first to preserve the body
|
||||
const clonedRequest = request.clone();
|
||||
|
||||
// Create new request with Authorization header and preserved body
|
||||
const modifiedRequest = new Request(clonedRequest, {
|
||||
// Clone first so the body stream is preserved on the new Request.
|
||||
const cloned = request.clone();
|
||||
const modified = new Request(cloned, {
|
||||
headers: {
|
||||
...Object.fromEntries(clonedRequest.headers),
|
||||
Authorization: token
|
||||
...Object.fromEntries(cloned.headers),
|
||||
Cookie: `fa_session=${sessionId}`
|
||||
}
|
||||
});
|
||||
|
||||
return fetch(modifiedRequest);
|
||||
return fetch(modified);
|
||||
}
|
||||
|
||||
return fetch(request);
|
||||
|
||||
Reference in New Issue
Block a user