浏览代码

Set PublicKeyCredentialCreationOptionsRepository by DSL or Bean

Closes gh-16396
Rob Winch 7 月之前
父节点
当前提交
683f1f4bc5

+ 27 - 0
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<H extends HttpSecurityBuilder<H>>
 
 	private boolean disableDefaultRegistrationPage = false;
 
+	private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository;
+
 	private HttpMessageConverter<Object> converter;
 
 	/**
@@ -130,6 +133,17 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		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(() -> {
@@ -141,6 +155,7 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		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<H extends HttpSecurityBuilder<H>>
 				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<H extends HttpSecurityBuilder<H>>
 		}
 	}
 
+	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));

+ 3 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/WebAuthnDsl.kt

@@ -18,6 +18,7 @@ package org.springframework.security.config.annotation.web
 
 import org.springframework.security.config.annotation.web.builders.HttpSecurity
 import org.springframework.security.config.annotation.web.configurers.WebAuthnConfigurer
+import org.springframework.security.web.webauthn.registration.PublicKeyCredentialCreationOptionsRepository
 
 /**
  * A Kotlin DSL to configure [HttpSecurity] webauthn using idiomatic Kotlin code.
@@ -35,6 +36,7 @@ class WebAuthnDsl {
     var rpId: String? = null
     var allowedOrigins: Set<String>? = null
     var disableDefaultRegistrationPage: Boolean? = false
+    var creationOptionsRepository: PublicKeyCredentialCreationOptionsRepository? = null
 
     internal fun get(): (WebAuthnConfigurer<HttpSecurity>) -> Unit {
         return { webAuthn ->
@@ -42,6 +44,7 @@ class WebAuthnDsl {
             rpId?.also { webAuthn.rpId(rpId) }
             allowedOrigins?.also { webAuthn.allowedOrigins(allowedOrigins) }
             disableDefaultRegistrationPage?.also { webAuthn.disableDefaultRegistrationPage(disableDefaultRegistrationPage!!) }
+            creationOptionsRepository?.also { webAuthn.creationOptionsRepository(creationOptionsRepository) }
         }
     }
 }

+ 99 - 0
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;
 
 /**
@@ -140,6 +142,46 @@ public class WebAuthnConfigurerTests {
 		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));
+	}
+
 	@Test
 	public void webauthnWhenConfiguredMessageConverter() throws Exception {
 		TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
@@ -165,6 +207,63 @@ public class WebAuthnConfigurerTests {
 			.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
 	@EnableWebSecurity
 	static class ConfigMessageConverter {

+ 38 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/web/WebAuthnDslTests.kt

@@ -30,6 +30,7 @@ import org.springframework.security.core.userdetails.User
 import org.springframework.security.core.userdetails.UserDetailsService
 import org.springframework.security.provisioning.InMemoryUserDetailsManager
 import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository
 import org.springframework.test.web.servlet.MockMvc
 import org.springframework.test.web.servlet.get
 import org.springframework.test.web.servlet.post
@@ -58,6 +59,16 @@ class WebAuthnDslTests {
                 }
     }
 
+    @Test
+    fun `explicit PublicKeyCredentialCreationOptionsRepository`() {
+        this.spring.register(ExplicitPublicKeyCredentialCreationOptionsRepositoryConfig::class.java).autowire()
+
+        this.mockMvc.post("/test1")
+            .andExpect {
+                status { isForbidden() }
+            }
+    }
+
     @Test
     fun `webauthn and formLogin configured with default registration page`() {
         spring.register(DefaultWebauthnConfig::class.java).autowire()
@@ -128,6 +139,33 @@ class WebAuthnDslTests {
         }
     }
 
+    @Configuration
+    @EnableWebSecurity
+    open class ExplicitPublicKeyCredentialCreationOptionsRepositoryConfig {
+        @Bean
+        open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+            http {
+                webAuthn {
+                    rpName = "Spring Security Relying Party"
+                    rpId = "example.com"
+                    allowedOrigins = setOf("https://example.com")
+                    creationOptionsRepository = HttpSessionPublicKeyCredentialCreationOptionsRepository()
+                }
+            }
+            return http.build()
+        }
+
+        @Bean
+        open fun userDetailsService(): UserDetailsService {
+            val userDetails = User.withDefaultPasswordEncoder()
+                .username("rod")
+                .password("password")
+                .roles("USER")
+                .build()
+            return InMemoryUserDetailsManager(userDetails)
+        }
+    }
+
     @Configuration
     @EnableWebSecurity
     open class WebauthnConfig {

+ 36 - 0
docs/modules/ROOT/pages/servlet/authentication/passkeys.adoc

@@ -60,6 +60,7 @@ Java::
 ----
 @Bean
 SecurityFilterChain filterChain(HttpSecurity http) {
+	// ...
 	http
 		// ...
 		.formLogin(withDefaults())
@@ -67,6 +68,8 @@ SecurityFilterChain filterChain(HttpSecurity http) {
 			.rpName("Spring Security Relying Party")
 			.rpId("example.com")
 			.allowedOrigins("https://example.com")
+			// optional properties
+			.creationOptionsRepository(new CustomPublicKeyCredentialCreationOptionsRepository())
 		);
 	return http.build();
 }
@@ -89,11 +92,14 @@ Kotlin::
 ----
 @Bean
 open fun filterChain(http: HttpSecurity): SecurityFilterChain {
+	// ...
 	http {
 		webAuthn {
 			rpName = "Spring Security Relying Party"
 			rpId = "example.com"
 			allowedOrigins = setOf("https://example.com")
+			// optional properties
+			creationOptionsRepository = CustomPublicKeyCredentialCreationOptionsRepository()
 		}
 	}
 }
@@ -110,6 +116,36 @@ open fun userDetailsService(): UserDetailsService {
 ----
 ======
 
+[[passkeys-configuration-pkccor]]
+=== Custom PublicKeyCredentialCreationOptionsRepository
+
+The `PublicKeyCredentialCreationOptionsRepository` is used to persist the `PublicKeyCredentialCreationOptions` between requests.
+The default is to persist it the `HttpSession`, but at times users may need to customize this behavior.
+This can be done by setting the optional property `creationOptionsRepository` demonstrated in xref:./passkeys.adoc#passkeys-configuration[Configuration] or by exposing a `PublicKeyCredentialCreationOptionsRepository` Bean:
+
+[tabs]
+======
+Java::
++
+[source,java,role="primary"]
+----
+@Bean
+CustomPublicKeyCredentialCreationOptionsRepository creationOptionsRepository() {
+	return new CustomPublicKeyCredentialCreationOptionsRepository();
+}
+----
+
+Kotlin::
++
+[source,kotlin,role="secondary"]
+----
+@Bean
+open fun creationOptionsRepository(): CustomPublicKeyCredentialCreationOptionsRepository {
+	return CustomPublicKeyCredentialCreationOptionsRepository()
+}
+----
+======
+
 [[passkeys-register]]
 == Register a New Credential
 

+ 4 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -14,3 +14,7 @@ Note that this may affect reports that operate on this key name.
 == OAuth
 
 * https://github.com/spring-projects/spring-security/pull/16386[gh-16386] - Enable PKCE for confidential clients using `ClientRegistration.clientSettings.requireProofKey=true` for xref:servlet/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[servlet] and xref:reactive/oauth2/client/core.adoc#oauth2Client-client-registration-requireProofKey[reactive] applications
+
+== WebAuthn
+
+* https://github.com/spring-projects/spring-security/pull/16396[gh-16396] - Added the ability to configure a custom xref:servlet/authentication/passkeys.adoc#passkeys-configuration-pkccor[`PublicKeyCredentialCreationOptionsRepository`]

+ 11 - 0
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