From 8181cec06c13007347f5604e90c99da2cb2151de Mon Sep 17 00:00:00 2001 From: DingHao Date: Sun, 12 Jan 2025 14:47:56 +0800 Subject: [PATCH 1/3] Set HttpMessageConverter by DSL Closes gh-16369 Signed-off-by: DingHao --- .../web/configurers/WebAuthnConfigurer.java | 27 ++++++- .../configurers/WebAuthnConfigurerTests.java | 79 ++++++++++++++++++- ...licKeyCredentialCreationOptionsFilter.java | 17 +++- 3 files changed, 117 insertions(+), 6 deletions(-) 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 1a955e523d..9ca5a3f6e7 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 @@ -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> private boolean disableDefaultRegistrationPage = false; + private HttpMessageConverter converter; + /** * The Relying Party id. * @param rpId the relying party id @@ -116,6 +119,16 @@ public class WebAuthnConfigurer> return this; } + /** + * Sets PublicKeyCredentialCreationOptionsRepository + * @param converter the creationOptionsRepository + * @return the {@link WebAuthnConfigurer} for further customization + */ + public WebAuthnConfigurer messageConverter(HttpMessageConverter converter) { + this.converter = converter; + return this; + } + @Override public void configure(H http) throws Exception { UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> { @@ -130,9 +143,17 @@ public class WebAuthnConfigurer> 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 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 a90c43f312..1e46d03c3f 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 @@ -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.io.IOException; import java.util.List; import org.junit.jupiter.api.Test; @@ -24,21 +25,37 @@ 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.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +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.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 +143,66 @@ 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 converter = new AbstractHttpMessageConverter<>() { + @Override + protected boolean supports(Class clazz) { + return true; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + protected void writeInternal(Object o, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + outputMessage.getBody().write("123".getBytes()); + } + }; + ConfigMessageConverter.converter = converter; + this.spring.register(ConfigMessageConverter.class).autowire(); + this.mvc.perform(post("/webauthn/register/options")) + .andExpect(status().isOk()) + .andExpect(content().string("123")); + } + + @Configuration + @EnableWebSecurity + static class ConfigMessageConverter { + + private static HttpMessageConverter 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 { 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 3f163b0cc2..965408621f 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 @@ -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 creating * 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 converter = new MappingJackson2HttpMessageConverter( + private HttpMessageConverter 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 converter) { + Assert.notNull(converter, "converter cannot be null"); + this.converter = converter; + } + } From 0d4f7864845b0660618f286ddd230e368cd07e92 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:28:26 -0600 Subject: [PATCH 2/3] Fix WebAuthnConfigurer Javadoc Issue gh-16397 --- .../annotation/web/configurers/WebAuthnConfigurer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 9ca5a3f6e7..79164a082c 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 @@ -120,8 +120,9 @@ public class WebAuthnConfigurer> } /** - * Sets PublicKeyCredentialCreationOptionsRepository - * @param converter the creationOptionsRepository + * 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 messageConverter(HttpMessageConverter converter) { From 5462b4c358b585f163312f2bc89b3454f995c6b2 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:29:07 -0600 Subject: [PATCH 3/3] webauthnWhenConfiguredMessageConverter uses mock Issue gh-16397 --- .../configurers/WebAuthnConfigurerTests.java | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) 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 1e46d03c3f..f51b20a094 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 @@ -16,7 +16,7 @@ package org.springframework.security.config.annotation.web.configurers; -import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.Test; @@ -25,12 +25,8 @@ 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.HttpInputMessage; import org.springframework.http.HttpOutputMessage; -import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -53,6 +49,7 @@ 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; @@ -153,29 +150,19 @@ public class WebAuthnConfigurerTests { WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class); ConfigMessageConverter.rpOperations = rpOperations; given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options); - HttpMessageConverter converter = new AbstractHttpMessageConverter<>() { - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - protected Object readInternal(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - return null; - } - - @Override - protected void writeInternal(Object o, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - outputMessage.getBody().write("123".getBytes()); - } - }; + HttpMessageConverter 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("123")); + .andExpect(content().string(expectedBody)); } @Configuration