From f4491f388ee886e4ada03c27c3858220023babab Mon Sep 17 00:00:00 2001 From: DingHao Date: Sun, 12 Jan 2025 14:11:02 +0800 Subject: [PATCH] Set PublicKeyCredentialCreationOptionsRepository by DSL or Bean Closes gh-16369 Signed-off-by: DingHao --- .../web/configurers/WebAuthnConfigurer.java | 27 +++++ .../configurers/WebAuthnConfigurerTests.java | 105 +++++++++++++++++- ...licKeyCredentialCreationOptionsFilter.java | 11 ++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java index 79164a082c..104a0be328 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -44,6 +44,7 @@ import org.springframework.security.web.webauthn.management.WebAuthnRelyingParty import org.springframework.security.web.webauthn.management.Webauthn4JRelyingPartyOperations; import org.springframework.security.web.webauthn.registration.DefaultWebAuthnRegistrationPageGeneratingFilter; import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsFilter; +import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsRepository; import org.springframework.security.web.webauthn.registration.WebAuthnRegistrationFilter; /** @@ -64,6 +65,8 @@ public class WebAuthnConfigurer> private boolean disableDefaultRegistrationPage = false; + private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository; + private HttpMessageConverter converter; /** @@ -130,6 +133,17 @@ public class WebAuthnConfigurer> return this; } + /** + * Sets PublicKeyCredentialCreationOptionsRepository + * @param creationOptionsRepository the creationOptionsRepository + * @return the {@link WebAuthnConfigurer} for further customization + */ + public WebAuthnConfigurer creationOptionsRepository( + PublicKeyCredentialCreationOptionsRepository creationOptionsRepository) { + this.creationOptionsRepository = creationOptionsRepository; + return this; + } + @Override public void configure(H http) throws Exception { UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> { @@ -141,6 +155,7 @@ public class WebAuthnConfigurer> UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class) .orElse(userCredentialRepository()); WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); + PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); webAuthnAuthnFilter.setAuthenticationManager( new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); @@ -148,6 +163,10 @@ public class WebAuthnConfigurer> rpOperations); PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter( rpOperations); + if (creationOptionsRepository != null) { + webAuthnRegistrationFilter.setCreationOptionsRepository(creationOptionsRepository); + creationOptionsFilter.setCreationOptionsRepository(creationOptionsRepository); + } if (this.converter != null) { webAuthnRegistrationFilter.setConverter(this.converter); creationOptionsFilter.setConverter(this.converter); @@ -181,6 +200,14 @@ public class WebAuthnConfigurer> } } + private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository() { + if (this.creationOptionsRepository != null) { + return this.creationOptionsRepository; + } + ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class); + return context.getBeanProvider(PublicKeyCredentialCreationOptionsRepository.class).getIfUnique(); + } + private Optional getSharedOrBean(H http, Class type) { C shared = http.getSharedObject(type); return Optional.ofNullable(shared).or(() -> getBeanOrNull(type)); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java index f51b20a094..37665b38da 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java @@ -43,6 +43,7 @@ import org.springframework.security.web.authentication.ui.DefaultResourcesFilter import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; +import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -55,6 +56,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -141,13 +143,53 @@ public class WebAuthnConfigurerTests { } @Test - public void webauthnWhenConfiguredMessageConverter() throws Exception { + public void webauthnWhenConfiguredPublicKeyCredentialCreationOptionsRepository() throws Exception { TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); SecurityContextHolder.setContext(new SecurityContextImpl(user)); PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions .createPublicKeyCredentialCreationOptions() .build(); WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class); + ConfigCredentialCreationOptionsRepository.rpOperations = rpOperations; + given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options); + String attrName = "attrName"; + HttpSessionPublicKeyCredentialCreationOptionsRepository creationOptionsRepository = new HttpSessionPublicKeyCredentialCreationOptionsRepository(); + creationOptionsRepository.setAttrName(attrName); + ConfigCredentialCreationOptionsRepository.creationOptionsRepository = creationOptionsRepository; + this.spring.register(ConfigCredentialCreationOptionsRepository.class).autowire(); + this.mvc.perform(post("/webauthn/register/options")) + .andExpect(status().isOk()) + .andExpect(request().sessionAttribute(attrName, options)); + } + + @Test + public void webauthnWhenConfiguredPublicKeyCredentialCreationOptionsRepositoryBeanPresent() throws Exception { + TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + SecurityContextHolder.setContext(new SecurityContextImpl(user)); + PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions + .createPublicKeyCredentialCreationOptions() + .build(); + WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class); + ConfigCredentialCreationOptionsRepositoryFromBean.rpOperations = rpOperations; + given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options); + String attrName = "attrName"; + HttpSessionPublicKeyCredentialCreationOptionsRepository creationOptionsRepository = new HttpSessionPublicKeyCredentialCreationOptionsRepository(); + creationOptionsRepository.setAttrName(attrName); + ConfigCredentialCreationOptionsRepositoryFromBean.creationOptionsRepository = creationOptionsRepository; + this.spring.register(ConfigCredentialCreationOptionsRepositoryFromBean.class).autowire(); + this.mvc.perform(post("/webauthn/register/options")) + .andExpect(status().isOk()) + .andExpect(request().sessionAttribute(attrName, options)); + } + + @Test + public void webauthnWhenConfiguredMessageConverter() throws Exception { + TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + SecurityContextHolder.setContext(new SecurityContextImpl(user)); + PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions + .createPublicKeyCredentialCreationOptions() + .build(); + WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class); ConfigMessageConverter.rpOperations = rpOperations; given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options); HttpMessageConverter converter = mock(HttpMessageConverter.class); @@ -161,8 +203,65 @@ public class WebAuthnConfigurerTests { ConfigMessageConverter.converter = converter; this.spring.register(ConfigMessageConverter.class).autowire(); this.mvc.perform(post("/webauthn/register/options")) - .andExpect(status().isOk()) - .andExpect(content().string(expectedBody)); + .andExpect(status().isOk()) + .andExpect(content().string(expectedBody)); + } + + @Configuration + @EnableWebSecurity + static class ConfigCredentialCreationOptionsRepository { + + private static HttpSessionPublicKeyCredentialCreationOptionsRepository creationOptionsRepository; + + private static WebAuthnRelyingPartyOperations rpOperations; + + @Bean + WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() { + return ConfigCredentialCreationOptionsRepository.rpOperations; + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.csrf(AbstractHttpConfigurer::disable) + .webAuthn((c) -> c.creationOptionsRepository(creationOptionsRepository)) + .build(); + } + + } + + @Configuration + @EnableWebSecurity + static class ConfigCredentialCreationOptionsRepositoryFromBean { + + private static HttpSessionPublicKeyCredentialCreationOptionsRepository creationOptionsRepository; + + private static WebAuthnRelyingPartyOperations rpOperations; + + @Bean + WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() { + return ConfigCredentialCreationOptionsRepositoryFromBean.rpOperations; + } + + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager(); + } + + @Bean + HttpSessionPublicKeyCredentialCreationOptionsRepository creationOptionsRepository() { + return ConfigCredentialCreationOptionsRepositoryFromBean.creationOptionsRepository; + } + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build(); + } + } @Configuration diff --git a/web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java b/web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java index 965408621f..0863925c8c 100644 --- a/web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java +++ b/web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java @@ -105,6 +105,17 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt this.converter.write(options, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response)); } + /** + * Sets the {@link PublicKeyCredentialCreationOptionsRepository} to use. The default + * is {@link HttpSessionPublicKeyCredentialCreationOptionsRepository}. + * @param creationOptionsRepository the + * {@link PublicKeyCredentialCreationOptionsRepository} to use. Cannot be null. + */ + public void setCreationOptionsRepository(PublicKeyCredentialCreationOptionsRepository creationOptionsRepository) { + Assert.notNull(creationOptionsRepository, "creationOptionsRepository cannot be null"); + this.repository = creationOptionsRepository; + } + /** * Set the {@link HttpMessageConverter} to read the * {@link WebAuthnRegistrationFilter.WebAuthnRegistrationRequest} and write the