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 1a955e523da..b1ff393ef5b 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 @@ -43,6 +43,7 @@ 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; /** @@ -63,6 +64,8 @@ public class WebAuthnConfigurer> private boolean disableDefaultRegistrationPage = false; + private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository; + /** * The Relying Party id. * @param rpId the relying party id @@ -116,6 +119,17 @@ public WebAuthnConfigurer disableDefaultRegistrationPage(boolean disable) { 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(() -> { @@ -127,12 +141,21 @@ public void configure(H http) throws Exception { 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))); + WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials, + rpOperations); + PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter( + rpOperations); + if (creationOptionsRepository != null) { + webAuthnRegistrationFilter.setCreationOptionsRepository(creationOptionsRepository); + creationOptionsFilter.setCreationOptionsRepository(creationOptionsRepository); + } http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class); - http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class); - http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class); + http.addFilterAfter(webAuthnRegistrationFilter, AuthorizationFilter.class); + http.addFilterBefore(creationOptionsFilter, AuthorizationFilter.class); http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class); DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http @@ -159,6 +182,14 @@ public void configure(H http) throws Exception { } } + 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 a90c43f3122..33912c9c75b 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 @@ -24,23 +24,35 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextImpl; 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.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; import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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; /** @@ -126,6 +138,103 @@ public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJa this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound()); } + @Test + 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)); + } + + @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 @EnableWebSecurity static class DefaultWebauthnConfiguration { 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 3f163b0cc2c..b072c262500 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 @@ -103,4 +103,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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; + } + }