diff --git a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java index 73e4225e5..d98ae8dd9 100644 --- a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java +++ b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/BootWebSecurityConfigurer.java @@ -75,7 +75,11 @@ public class BootWebSecurityConfigurer { if (enableCsrf) { CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); csrfTokenRepository.setCookiePath("/"); - http.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository)); + http.csrf( + csrf -> + csrf.csrfTokenRepository(csrfTokenRepository) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())) + .addFilterAfter(new CsrfCookieFilter(), SpringSecurityToJaasFilter.class); } else { http.csrf(AbstractHttpConfigurer::disable).httpBasic(Customizer.withDefaults()); } diff --git a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/CsrfCookieFilter.java b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/CsrfCookieFilter.java new file mode 100644 index 000000000..2af0127be --- /dev/null +++ b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/CsrfCookieFilter.java @@ -0,0 +1,25 @@ +package pro.taskana.example.boot.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; + +final class CsrfCookieFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, + @SuppressWarnings("NullableProblems") HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf"); + // Render the token value to a cookie by causing the deferred token to be loaded + csrfToken.getToken(); + + filterChain.doFilter(request, response); + } +} diff --git a/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/SpaCsrfTokenRequestHandler.java b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/SpaCsrfTokenRequestHandler.java new file mode 100644 index 000000000..2b3303749 --- /dev/null +++ b/rest/taskana-rest-spring-example-boot/src/main/java/pro/taskana/example/boot/security/SpaCsrfTokenRequestHandler.java @@ -0,0 +1,44 @@ +package pro.taskana.example.boot.security; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.function.Supplier; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.csrf.CsrfTokenRequestHandler; +import org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler; +import org.springframework.util.StringUtils; + +final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { + private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler(); + + @Override + public void handle( + HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { + /* + * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of + * the CsrfToken when it is rendered in the response body. + */ + this.delegate.handle(request, response, csrfToken); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + /* + * If the request contains a request header, use CsrfTokenRequestAttributeHandler + * to resolve the CsrfToken. This applies when a single-page application includes + * the header value automatically, which was obtained via a cookie containing the + * raw CsrfToken. + */ + if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) { + return super.resolveCsrfTokenValue(request, csrfToken); + } + /* + * In all other cases (e.g. if the request contains a request parameter), use + * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies + * when a server-side rendered form includes the _csrf request parameter as a + * hidden input. + */ + return this.delegate.resolveCsrfTokenValue(request, csrfToken); + } +}