Some checks failed
- frontend/login: derive cookie `secure` flag from request URL protocol.
Pre-PR the cookie was only read by SSR so the flag didn't matter; now
the cookie IS the API credential and must be Secure on HTTPS or it
leaks a 24h Basic token on plaintext networks. Dev runs over HTTP and
would silently lose the cookie if we hardcoded `secure: true`, so the
flag follows `event.url.protocol === 'https:'`.
- SecurityConfig: rewrite the CSRF-disabled comment. The old
"browsers block cross-origin custom headers" justification no longer
holds once /api/* is authenticated via the cookie. Make the
load-bearing dependencies explicit: SameSite=strict on the auth_token
cookie + Spring's default CORS rejection.
- AuthTokenCookieFilter:
- Scope to /api/* only. /actuator/health and similar must not be
cookie-authenticated.
- Refuse malformed percent-encoding (URLDecoder throws); forward the
request without a promoted Authorization rather than crash.
- Use isBlank() instead of isEmpty() per Nora.
- Javadoc warning: getHeaderNames/getHeaders exposes the Basic
credential; any future header-iterating logger must scrub
Authorization before logging.
- Tests: add `passes_through_unchanged_when_request_is_outside_api_scope`
(/actuator/health with cookie should NOT be wrapped) and
`passes_through_unchanged_when_cookie_value_is_malformed_percent_encoding`.
Tighten the explicit-header test to verify same-instance forwarding
rather than just header equality.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
61 lines
2.0 KiB
TypeScript
61 lines
2.0 KiB
TypeScript
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
|
import { env } from '$env/dynamic/private';
|
|
import { getErrorMessage } from '$lib/shared/errors';
|
|
import type { PageServerLoad } from './$types';
|
|
|
|
export const load: PageServerLoad = ({ url }) => {
|
|
return { registered: url.searchParams.get('registered') === '1' };
|
|
};
|
|
|
|
export const actions = {
|
|
login: async ({ request, cookies, fetch, url }) => {
|
|
const data = await request.formData();
|
|
const email = data.get('email') as string;
|
|
const password = data.get('password') as string;
|
|
|
|
if (!email || !password) {
|
|
return fail(400, { error: getErrorMessage('MISSING_CREDENTIALS') });
|
|
}
|
|
|
|
const credentials = btoa(`${email}:${password}`);
|
|
const authHeader = `Basic ${credentials}`;
|
|
|
|
// Raw fetch is intentional here: we need to pass an explicit Authorization
|
|
// header built from the form data, not the cookie-based auth used elsewhere.
|
|
try {
|
|
const baseUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
|
const response = await fetch(`${baseUrl}/api/users/me`, {
|
|
method: 'GET',
|
|
headers: { Authorization: authHeader }
|
|
});
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
return fail(401, { error: getErrorMessage('UNAUTHORIZED') });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
|
}
|
|
|
|
// The cookie IS the API credential — promoted to `Authorization: Basic …`
|
|
// on every browser → backend request by AuthTokenCookieFilter on the
|
|
// Spring side (see #520). It must be Secure on HTTPS or it leaks
|
|
// a 24h Basic token on plaintext networks. Dev runs over HTTP and
|
|
// would silently lose the cookie if we hardcoded secure=true.
|
|
const isHttps = url.protocol === 'https:';
|
|
cookies.set('auth_token', authHeader, {
|
|
path: '/',
|
|
httpOnly: true,
|
|
sameSite: 'strict',
|
|
secure: isHttps,
|
|
maxAge: 60 * 60 * 24
|
|
});
|
|
} catch (e) {
|
|
console.error(e);
|
|
return fail(500, { error: getErrorMessage('INTERNAL_ERROR') });
|
|
}
|
|
|
|
return redirect(303, '/');
|
|
}
|
|
} satisfies Actions;
|