diff --git a/backend/src/main/java/org/raddatz/familienarchiv/security/AuthTokenCookieFilter.java b/backend/src/main/java/org/raddatz/familienarchiv/security/AuthTokenCookieFilter.java deleted file mode 100644 index d382f872..00000000 --- a/backend/src/main/java/org/raddatz/familienarchiv/security/AuthTokenCookieFilter.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.raddatz.familienarchiv.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.Enumeration; - -/** - * Promotes the {@code auth_token} cookie to an {@code Authorization} header - * so that browser-side requests to {@code /api/*} authenticate the same way - * SSR fetches do. - * - *
The SvelteKit login action stores the full HTTP Basic header value
- * ({@code "Basic This filter closes that gap: if a request has an {@code auth_token}
- * cookie but no explicit {@code Authorization} header, promote the cookie
- * value (URL-decoded) into the header before Spring Security inspects it.
- * Explicit {@code Authorization} headers are preserved unchanged.
- *
- * See #520. Filter runs at {@code Ordered.HIGHEST_PRECEDENCE} so it
- * mutates the request before any Spring Security filter sees it.
- *
- * Scope: only {@code /api/*} requests are touched. The
- * {@code /actuator/*} block in Caddy plus the open auth/reset paths in
- * {@link SecurityConfig} must NOT receive a promoted Authorization.
- *
- * ⚠ Log-leakage warning: the wrapped request exposes the
- * Authorization header via {@code getHeaderNames}/{@code getHeaders}. Any
- * filter or interceptor that iterates request headers will see the live
- * Basic credential. Do NOT add a request-header logger downstream of this
- * filter without explicitly scrubbing the {@code Authorization} field.
- */
-@Component
-@Order(org.springframework.core.Ordered.HIGHEST_PRECEDENCE)
-public class AuthTokenCookieFilter extends OncePerRequestFilter {
-
- static final String COOKIE_NAME = "auth_token";
- static final String SCOPE_PREFIX = "/api/";
-
- @Override
- protected void doFilterInternal(HttpServletRequest request,
- HttpServletResponse response,
- FilterChain chain) throws ServletException, IOException {
- // Scope: only /api/* needs cookie promotion. /actuator/health (open),
- // /api/auth/forgot-password (open), /login etc. don't.
- if (!request.getRequestURI().startsWith(SCOPE_PREFIX)) {
- chain.doFilter(request, response);
- return;
- }
- // An explicit Authorization header wins — this is the SSR fetch path
- // (hooks.server.ts builds the header itself).
- if (request.getHeader(HttpHeaders.AUTHORIZATION) != null) {
- chain.doFilter(request, response);
- return;
- }
- Cookie[] cookies = request.getCookies();
- if (cookies == null) {
- chain.doFilter(request, response);
- return;
- }
- for (Cookie c : cookies) {
- if (COOKIE_NAME.equals(c.getName()) && c.getValue() != null && !c.getValue().isBlank()) {
- String decoded;
- try {
- decoded = URLDecoder.decode(c.getValue(), StandardCharsets.UTF_8);
- } catch (IllegalArgumentException malformed) {
- // Malformed percent-encoding — refuse to forward a bogus
- // Authorization header. Spring Security will treat the
- // request as unauthenticated.
- chain.doFilter(request, response);
- return;
- }
- chain.doFilter(new AuthHeaderRequest(request, decoded), response);
- return;
- }
- }
- chain.doFilter(request, response);
- }
-
- /**
- * Adds (or overrides) the {@code Authorization} header on a wrapped request.
- * All other headers pass through unchanged.
- */
- static final class AuthHeaderRequest extends HttpServletRequestWrapper {
- private final String authorization;
-
- AuthHeaderRequest(HttpServletRequest request, String authorization) {
- super(request);
- this.authorization = authorization;
- }
-
- @Override
- public String getHeader(String name) {
- if (HttpHeaders.AUTHORIZATION.equalsIgnoreCase(name)) {
- return authorization;
- }
- return super.getHeader(name);
- }
-
- @Override
- public Enumeration See #520.
- */
-class AuthTokenCookieFilterTest {
-
- private final AuthTokenCookieFilter filter = new AuthTokenCookieFilter();
-
- @Test
- void promotes_url_encoded_auth_token_cookie_to_decoded_Authorization_header() throws Exception {
- MockHttpServletRequest req = new MockHttpServletRequest();
- req.setRequestURI("/api/users/me");
- req.setCookies(new Cookie("auth_token", "Basic%20YWRtaW5AZmFtaWx5YXJjaGl2ZS5sb2NhbDpzZWNyZXQ%3D"));
- MockHttpServletResponse res = new MockHttpServletResponse();
- FilterChain chain = mock(FilterChain.class);
-
- filter.doFilter(req, res, chain);
-
- ArgumentCaptor