Skip to content

Commit

Permalink
Set PublicKeyCredentialCreationOptionsRepository by DSL or Bean
Browse files Browse the repository at this point in the history
Closes gh-16369

Signed-off-by: DingHao <[email protected]>
  • Loading branch information
kse-music committed Jan 12, 2025
1 parent 72a2831 commit 462ecb5
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -63,6 +64,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>

private boolean disableDefaultRegistrationPage = false;

private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository;

/**
* The Relying Party id.
* @param rpId the relying party id
Expand Down Expand Up @@ -116,6 +119,17 @@ public WebAuthnConfigurer<H> disableDefaultRegistrationPage(boolean disable) {
return this;
}

/**
* Sets PublicKeyCredentialCreationOptionsRepository
* @param creationOptionsRepository the creationOptionsRepository
* @return the {@link WebAuthnConfigurer} for further customization
*/
public WebAuthnConfigurer<H> creationOptionsRepository(
PublicKeyCredentialCreationOptionsRepository creationOptionsRepository) {
this.creationOptionsRepository = creationOptionsRepository;
return this;
}

@Override
public void configure(H http) throws Exception {
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
Expand All @@ -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
Expand All @@ -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 <C> Optional<C> getSharedOrBean(H http, Class<C> type) {
C shared = http.getSharedObject(type);
return Optional.ofNullable(shared).or(() -> getBeanOrNull(type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}

0 comments on commit 462ecb5

Please sign in to comment.