Selaa lähdekoodia

webauthn: introduce WebAuthnConfigurer#disableDefaultRegistrationPage

Daniel Garnier-Moiroux 9 kuukautta sitten
vanhempi
commit
2639ac6545

+ 28 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java

@@ -61,6 +61,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 
 	private Set<String> allowedOrigins = new HashSet<>();
 
+	private boolean disableDefaultRegistrationPage = false;
+
 	/**
 	 * The Relying Party id.
 	 * @param rpId the relying party id
@@ -102,6 +104,18 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		return this;
 	}
 
+	/**
+	 * Configures whether the default webauthn registration should be disabled. Setting it
+	 * to {@code true} will prevent the configurer from registering the
+	 * {@link DefaultWebAuthnRegistrationPageGeneratingFilter}.
+	 * @param disable disable the default registration page if true, enable it otherwise
+	 * @return the {@link WebAuthnConfigurer} for further customization
+	 */
+	public WebAuthnConfigurer<H> disableDefaultRegistrationPage(boolean disable) {
+		this.disableDefaultRegistrationPage = disable;
+		return this;
+	}
+
 	@Override
 	public void configure(H http) throws Exception {
 		UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
@@ -119,17 +133,28 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
 		http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class);
 		http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class);
-		http.addFilterAfter(new DefaultWebAuthnRegistrationPageGeneratingFilter(userEntities, userCredentials),
-				AuthorizationFilter.class);
 		http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
+
 		DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
 			.getSharedObject(DefaultLoginPageGeneratingFilter.class);
-		if (loginPageGeneratingFilter != null) {
+		boolean isLoginPageEnabled = loginPageGeneratingFilter != null && loginPageGeneratingFilter.isEnabled();
+		if (isLoginPageEnabled) {
 			loginPageGeneratingFilter.setPasskeysEnabled(true);
 			loginPageGeneratingFilter.setResolveHeaders((request) -> {
 				CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
 				return Map.of(csrfToken.getHeaderName(), csrfToken.getToken());
 			});
+		}
+
+		if (!this.disableDefaultRegistrationPage) {
+			http.addFilterAfter(new DefaultWebAuthnRegistrationPageGeneratingFilter(userEntities, userCredentials),
+					AuthorizationFilter.class);
+			if (!isLoginPageEnabled) {
+				http.addFilter(DefaultResourcesFilter.css());
+			}
+		}
+
+		if (isLoginPageEnabled || !this.disableDefaultRegistrationPage) {
 			http.addFilter(DefaultResourcesFilter.webauthn());
 		}
 	}

+ 121 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java

@@ -16,6 +16,8 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.List;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -29,9 +31,12 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.FilterChainProxy;
 import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
 import org.springframework.test.web.servlet.MockMvc;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@@ -50,14 +55,77 @@ public class WebAuthnConfigurerTests {
 	MockMvc mvc;
 
 	@Test
-	public void javascriptWhenWebauthnConfiguredThenServesJavascript() throws Exception {
+	public void webauthnWhenConfiguredConfiguredThenServesJavascript() throws Exception {
+		this.spring.register(DefaultWebauthnConfiguration.class).autowire();
+		this.mvc.perform(get("/login/webauthn.js"))
+			.andExpect(status().isOk())
+			.andExpect(header().string("content-type", "text/javascript;charset=UTF-8"))
+			.andExpect(content().string(containsString("async function authenticate(")));
+	}
+
+	@Test
+	public void webauthnWhenConfiguredConfiguredThenServesCss() throws Exception {
+		this.spring.register(DefaultWebauthnConfiguration.class).autowire();
+		this.mvc.perform(get("/default-ui.css"))
+			.andExpect(status().isOk())
+			.andExpect(header().string("content-type", "text/css;charset=UTF-8"))
+			.andExpect(content().string(containsString("body {")));
+	}
+
+	@Test
+	public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesJavascript() throws Exception {
+		this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire();
+		this.mvc.perform(get("/login/webauthn.js"))
+			.andExpect(status().isOk())
+			.andExpect(header().string("content-type", "text/javascript;charset=UTF-8"))
+			.andExpect(content().string(containsString("async function authenticate(")));
+	}
+
+	@Test
+	public void webauthnWhenNoFormLoginAndDefaultRegistrationPageConfiguredThenServesCss() throws Exception {
+		this.spring.register(NoFormLoginAndDefaultRegistrationPageConfiguration.class).autowire();
+		this.mvc.perform(get("/default-ui.css"))
+			.andExpect(status().isOk())
+			.andExpect(header().string("content-type", "text/css;charset=UTF-8"))
+			.andExpect(content().string(containsString("body {")));
+	}
+
+	@Test
+	public void webauthnWhenFormLoginAndDefaultRegistrationPageConfiguredThenNoDuplicateFilters() {
 		this.spring.register(DefaultWebauthnConfiguration.class).autowire();
+		FilterChainProxy filterChain = this.spring.getContext().getBean(FilterChainProxy.class);
+
+		List<DefaultResourcesFilter> defaultResourcesFilters = filterChain.getFilterChains()
+			.get(0)
+			.getFilters()
+			.stream()
+			.filter(DefaultResourcesFilter.class::isInstance)
+			.map(DefaultResourcesFilter.class::cast)
+			.toList();
+
+		assertThat(defaultResourcesFilters).map(DefaultResourcesFilter::toString)
+			.filteredOn((filterDescription) -> filterDescription.contains("login/webauthn.js"))
+			.hasSize(1);
+		assertThat(defaultResourcesFilters).map(DefaultResourcesFilter::toString)
+			.filteredOn((filterDescription) -> filterDescription.contains("default-ui.css"))
+			.hasSize(1);
+	}
+
+	@Test
+	public void webauthnWhenConfiguredAndFormLoginThenDoesServesJavascript() throws Exception {
+		this.spring.register(FormLoginAndNoDefaultRegistrationPageConfiguration.class).autowire();
 		this.mvc.perform(get("/login/webauthn.js"))
 			.andExpect(status().isOk())
 			.andExpect(header().string("content-type", "text/javascript;charset=UTF-8"))
 			.andExpect(content().string(containsString("async function authenticate(")));
 	}
 
+	@Test
+	public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJavascript() throws Exception {
+		this.spring.register(NoDefaultRegistrationPageConfiguration.class).autowire();
+		this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound());
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class DefaultWebauthnConfiguration {
@@ -67,6 +135,22 @@ public class WebAuthnConfigurerTests {
 			return new InMemoryUserDetailsManager();
 		}
 
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			return http.formLogin(Customizer.withDefaults()).webAuthn(Customizer.withDefaults()).build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class NoFormLoginAndDefaultRegistrationPageConfiguration {
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return new InMemoryUserDetailsManager();
+		}
+
 		@Bean
 		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 			return http.webAuthn(Customizer.withDefaults()).build();
@@ -74,4 +158,40 @@ public class WebAuthnConfigurerTests {
 
 	}
 
+	@Configuration
+	@EnableWebSecurity
+	static class FormLoginAndNoDefaultRegistrationPageConfiguration {
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return new InMemoryUserDetailsManager();
+		}
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			return http.formLogin(Customizer.withDefaults())
+				.webAuthn((webauthn) -> webauthn.disableDefaultRegistrationPage(true))
+				.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class NoDefaultRegistrationPageConfiguration {
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return new InMemoryUserDetailsManager();
+		}
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			return http.formLogin((login) -> login.loginPage("/custom-login-page"))
+				.webAuthn((webauthn) -> webauthn.disableDefaultRegistrationPage(true))
+				.build();
+		}
+
+	}
+
 }