Skip to content

Commit

Permalink
webauthn: introduce WebAuthnConfigurer#disableDefaultRegistrationPage
Browse files Browse the repository at this point in the history
  • Loading branch information
Kehrlann authored and rwinch committed Nov 14, 2024
1 parent de7c452 commit 2639ac6
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -102,6 +104,18 @@ public WebAuthnConfigurer<H> allowedOrigins(Set<String> allowedOrigins) {
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(() -> {
Expand All @@ -119,17 +133,28 @@ public void configure(H http) throws Exception {
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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,9 +31,12 @@
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;
Expand All @@ -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 {
Expand All @@ -67,11 +135,63 @@ UserDetailsService userDetailsService() {
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();
}

}

@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();
}

}

}

0 comments on commit 2639ac6

Please sign in to comment.