From 2010988dc2d48c632c23520d44f0666a234d133a Mon Sep 17 00:00:00 2001 From: Sven Mitt Date: Fri, 14 Aug 2026 11:05:36 +0300 Subject: [PATCH] feat: add spa support for CSRF by setting into cookie WE2-1240 Signed-off-by: Sven Mitt --- example/README.md | 4 +- .../config/ApplicationConfiguration.java | 41 ++++++++++++ .../src/main/resources/application-dev.yaml | 2 + example/src/main/resources/static/js/csrf.js | 37 +++++++++++ .../src/main/resources/templates/index.html | 6 +- .../src/main/resources/templates/welcome.html | 10 ++- .../example/SpaCsrfConfigurationTest.java | 66 +++++++++++++++++++ .../eu/webeid/example/WebApplicationTest.java | 11 +++- 8 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 example/src/main/resources/static/js/csrf.js create mode 100644 example/src/test/java/eu/webeid/example/SpaCsrfConfigurationTest.java diff --git a/example/README.md b/example/README.md index b0b0e6a1..11fd9dd7 100644 --- a/example/README.md +++ b/example/README.md @@ -142,7 +142,9 @@ The main configuration file `src/main/resources/application.yaml` is shared by a Besides configuration settings, the trusted certificate authority certificates may need to be configured as described in section [_3. Configure the trusted certificate authority certificates_](#3-configure-the-trusted-certificate-authority-certificates) above. -Spring Security has CSRF protection enabled by default. Web eID requires CSRF protection. +Spring Security has CSRF protection enabled by default. Web eID requires CSRF protection. By default, the frontend reads +CSRF tokens from Thymeleaf meta tags. Set `web-eid-auth-token.csrf.use-spa-configuration=true` to use Spring Security's +SPA-compatible CSRF setup with a JavaScript-readable `XSRF-TOKEN` cookie. ### Integration with Web eID components diff --git a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java index 1728628b..5cc34045 100644 --- a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java @@ -24,6 +24,9 @@ import eu.webeid.example.security.AuthTokenDTOAuthenticationProvider; import eu.webeid.example.security.WebEidAjaxLoginProcessingFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; @@ -34,17 +37,38 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +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; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.function.Supplier; + @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true) public class ApplicationConfiguration implements WebMvcConfigurer { + private final boolean useSpaCsrfConfiguration; + + public ApplicationConfiguration(@Value("${web-eid-auth-token.csrf.use-spa-configuration:false}") String useSpaCsrfConfiguration) { + this.useSpaCsrfConfiguration = Boolean.TRUE.toString().equalsIgnoreCase(useSpaCsrfConfiguration); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception { return http + .csrf(csrf -> { + if (useSpaCsrfConfiguration) { + csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()); + } + }) .authenticationProvider(authTokenDTOAuthenticationProvider) .addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class) @@ -59,4 +83,21 @@ public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/welcome").setViewName("welcome"); } + private static final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler { + private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler(); + private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler(); + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { + xor.handle(request, response, csrfToken); + csrfToken.get(); + } + + @Override + public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { + String headerValue = request.getHeader(csrfToken.getHeaderName()); + return (StringUtils.hasText(headerValue) ? plain : xor).resolveCsrfTokenValue(request, csrfToken); + } + } + } diff --git a/example/src/main/resources/application-dev.yaml b/example/src/main/resources/application-dev.yaml index 9c637326..9bda489c 100644 --- a/example/src/main/resources/application-dev.yaml +++ b/example/src/main/resources/application-dev.yaml @@ -1,4 +1,6 @@ web-eid-auth-token: + csrf: + use-spa-configuration: true validation: use-digidoc4j-prod-configuration: false local-origin: "https://test.web-eid.eu" diff --git a/example/src/main/resources/static/js/csrf.js b/example/src/main/resources/static/js/csrf.js new file mode 100644 index 00000000..586ccc7f --- /dev/null +++ b/example/src/main/resources/static/js/csrf.js @@ -0,0 +1,37 @@ +"use strict"; + +const CSRF_COOKIE_NAME = "XSRF-TOKEN"; +const CSRF_COOKIE_HEADER_NAME = "X-XSRF-TOKEN"; + +export function csrfHeader() { + const cookieToken = getCookie(CSRF_COOKIE_NAME); + if (cookieToken) { + return {[CSRF_COOKIE_HEADER_NAME]: cookieToken}; + } + + const metaToken = document.querySelector("#csrftoken")?.content; + const metaHeaderName = document.querySelector("#csrfheadername")?.content; + if (metaToken && metaHeaderName) { + return {[metaHeaderName]: metaToken}; + } + + return {}; +} + +function getCookie(name) { + const encodedName = encodeURIComponent(name) + "="; + return document.cookie + .split(";") + .map(cookie => cookie.trim()) + .filter(cookie => cookie.startsWith(encodedName)) + .map(cookie => decodeCookieValue(cookie.substring(encodedName.length))) + .shift(); +} + +function decodeCookieValue(value) { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} diff --git a/example/src/main/resources/templates/index.html b/example/src/main/resources/templates/index.html index fd28a8da..f5dccfdb 100644 --- a/example/src/main/resources/templates/index.html +++ b/example/src/main/resources/templates/index.html @@ -248,15 +248,13 @@

For developers