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..8591c534 100644 --- a/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ApplicationConfiguration.java @@ -24,8 +24,13 @@ 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.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -34,16 +39,37 @@ 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.CorsRegistry; 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 { @Bean - public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, AuthenticationConfiguration authConfig) throws Exception { + public SecurityFilterChain filterChain( + HttpSecurity http, + AuthTokenDTOAuthenticationProvider authTokenDTOAuthenticationProvider, + AuthenticationConfiguration authConfig, + @Value("${web-eid-auth-token.csrf.use-spa-configuration:false}") boolean useSpaCsrfConfiguration + ) throws Exception { + if (useSpaCsrfConfiguration) { + http + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())) + .cors(Customizer.withDefaults()); + } return http .authenticationProvider(authTokenDTOAuthenticationProvider) .addFilterBefore(new WebEidAjaxLoginProcessingFilter("/auth/login", authConfig.getAuthenticationManager()), @@ -53,10 +79,42 @@ public SecurityFilterChain filterChain(HttpSecurity http, AuthTokenDTOAuthentica .build(); } + @ConditionalOnProperty(name = "web-eid-auth-token.csrf.use-spa-configuration", havingValue = "true") + @Bean + public WebMvcConfigurer corsConfigurer(YAMLConfig config) { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(config.getCorsAllowedOrigin()) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } + @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); 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/java/eu/webeid/example/config/ValidationConfiguration.java b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java index 0a6c1ad7..597df847 100644 --- a/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/ValidationConfiguration.java @@ -91,11 +91,6 @@ public AuthTokenValidator validator(YAMLConfig yamlConfig) { } } - @Bean - public YAMLConfig yamlConfig() { - return new YAMLConfig(); - } - private X509Certificate[] loadTrustedCACertificatesFromCerFiles() { List caCertificates = new ArrayList<>(); diff --git a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java index 1c3359ae..db71850e 100644 --- a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java +++ b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java @@ -36,6 +36,8 @@ public class YAMLConfig { @Value("local-origin") private String localOrigin; + private String corsAllowedOrigin; + @Value("site-cert-hash") private String siteCertHash; @@ -53,6 +55,11 @@ public String getLocalOrigin() { public void setLocalOrigin(String localOrigin) { this.localOrigin = localOrigin; + this.corsAllowedOrigin = localOrigin; + } + + public String getCorsAllowedOrigin() { + return corsAllowedOrigin; } public String getSiteCertHash() { 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