2
0
Эх сурвалжийг харах

WebAuthnConfigurer Supports HttpMessageConverter

Closes gh-16397
Rob Winch 7 сар өмнө
parent
commit
4dc1dcbf24

+ 25 - 3
config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.Set;
 
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.context.ApplicationContext;
+import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.core.userdetails.UserDetailsService;
@@ -63,6 +64,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 
 	private boolean disableDefaultRegistrationPage = false;
 
+	private HttpMessageConverter<Object> converter;
+
 	/**
 	 * The Relying Party id.
 	 * @param rpId the relying party id
@@ -116,6 +119,17 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		return this;
 	}
 
+	/**
+	 * Sets {@link HttpMessageConverter} used for WebAuthn to read/write to the HTTP
+	 * request/response.
+	 * @param converter the {@link HttpMessageConverter}
+	 * @return the {@link WebAuthnConfigurer} for further customization
+	 */
+	public WebAuthnConfigurer<H> messageConverter(HttpMessageConverter<Object> converter) {
+		this.converter = converter;
+		return this;
+	}
+
 	@Override
 	public void configure(H http) throws Exception {
 		UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
@@ -130,9 +144,17 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
 		WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
 		webAuthnAuthnFilter.setAuthenticationManager(
 				new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
+		WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
+				rpOperations);
+		PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
+				rpOperations);
+		if (this.converter != null) {
+			webAuthnRegistrationFilter.setConverter(this.converter);
+			creationOptionsFilter.setConverter(this.converter);
+		}
 		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

+ 65 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
@@ -24,21 +25,34 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.converter.HttpMessageConverter;
+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.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.BDDMockito.willAnswer;
+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.status;
@@ -126,6 +140,56 @@ public class WebAuthnConfigurerTests {
 		this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound());
 	}
 
+	@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<Object> converter = mock(HttpMessageConverter.class);
+		given(converter.canWrite(any(), any())).willReturn(true);
+		String expectedBody = "123";
+		willAnswer((args) -> {
+			HttpOutputMessage out = (HttpOutputMessage) args.getArguments()[2];
+			out.getBody().write(expectedBody.getBytes(StandardCharsets.UTF_8));
+			return null;
+		}).given(converter).write(any(), any(), any());
+		ConfigMessageConverter.converter = converter;
+		this.spring.register(ConfigMessageConverter.class).autowire();
+		this.mvc.perform(post("/webauthn/register/options"))
+			.andExpect(status().isOk())
+			.andExpect(content().string(expectedBody));
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class ConfigMessageConverter {
+
+		private static HttpMessageConverter<Object> converter;
+
+		private static WebAuthnRelyingPartyOperations rpOperations;
+
+		@Bean
+		WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
+			return ConfigMessageConverter.rpOperations;
+		}
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return new InMemoryUserDetailsManager();
+		}
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			return http.csrf(AbstractHttpConfigurer::disable).webAuthn((c) -> c.messageConverter(converter)).build();
+		}
+
+	}
+
 	@Configuration
 	@EnableWebSecurity
 	static class DefaultWebauthnConfiguration {

+ 15 - 2
web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -53,6 +53,8 @@ import static org.springframework.security.web.util.matcher.AntPathRequestMatche
  * {@link PublicKeyCredentialCreationOptions} for <a href=
  * "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
  * a new credential.
+ *
+ * @author DingHao
  */
 public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilter {
 
@@ -67,7 +69,7 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt
 
 	private final WebAuthnRelyingPartyOperations rpOperations;
 
-	private final HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
+	private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
 			Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
 
 	/**
@@ -103,4 +105,15 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt
 		this.converter.write(options, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
 	}
 
+	/**
+	 * Set the {@link HttpMessageConverter} to read the
+	 * {@link WebAuthnRegistrationFilter.WebAuthnRegistrationRequest} and write the
+	 * response. The default is {@link MappingJackson2HttpMessageConverter}.
+	 * @param converter the {@link HttpMessageConverter} to use. Cannot be null.
+	 */
+	public void setConverter(HttpMessageConverter<Object> converter) {
+		Assert.notNull(converter, "converter cannot be null");
+		this.converter = converter;
+	}
+
 }