Skip to content

Commit 4dc1dcb

Browse files
committed
WebAuthnConfigurer Supports HttpMessageConverter
Closes gh-16397
2 parents 4fc99aa + 5462b4c commit 4dc1dcb

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
2525
import org.springframework.context.ApplicationContext;
26+
import org.springframework.http.converter.HttpMessageConverter;
2627
import org.springframework.security.authentication.ProviderManager;
2728
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2829
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -63,6 +64,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
6364

6465
private boolean disableDefaultRegistrationPage = false;
6566

67+
private HttpMessageConverter<Object> converter;
68+
6669
/**
6770
* The Relying Party id.
6871
* @param rpId the relying party id
@@ -116,6 +119,17 @@ public WebAuthnConfigurer<H> disableDefaultRegistrationPage(boolean disable) {
116119
return this;
117120
}
118121

122+
/**
123+
* Sets {@link HttpMessageConverter} used for WebAuthn to read/write to the HTTP
124+
* request/response.
125+
* @param converter the {@link HttpMessageConverter}
126+
* @return the {@link WebAuthnConfigurer} for further customization
127+
*/
128+
public WebAuthnConfigurer<H> messageConverter(HttpMessageConverter<Object> converter) {
129+
this.converter = converter;
130+
return this;
131+
}
132+
119133
@Override
120134
public void configure(H http) throws Exception {
121135
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
@@ -130,9 +144,17 @@ public void configure(H http) throws Exception {
130144
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
131145
webAuthnAuthnFilter.setAuthenticationManager(
132146
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
147+
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
148+
rpOperations);
149+
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
150+
rpOperations);
151+
if (this.converter != null) {
152+
webAuthnRegistrationFilter.setConverter(this.converter);
153+
creationOptionsFilter.setConverter(this.converter);
154+
}
133155
http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
134-
http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class);
135-
http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class);
156+
http.addFilterAfter(webAuthnRegistrationFilter, AuthorizationFilter.class);
157+
http.addFilterBefore(creationOptionsFilter, AuthorizationFilter.class);
136158
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
137159

138160
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http

config/src/test/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurerTests.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.security.config.annotation.web.configurers;
1818

19+
import java.nio.charset.StandardCharsets;
1920
import java.util.List;
2021

2122
import org.junit.jupiter.api.Test;
@@ -24,21 +25,34 @@
2425
import org.springframework.beans.factory.annotation.Autowired;
2526
import org.springframework.context.annotation.Bean;
2627
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.http.HttpOutputMessage;
29+
import org.springframework.http.converter.HttpMessageConverter;
30+
import org.springframework.security.authentication.TestingAuthenticationToken;
2731
import org.springframework.security.config.Customizer;
2832
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
2933
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3034
import org.springframework.security.config.test.SpringTestContext;
3135
import org.springframework.security.config.test.SpringTestContextExtension;
36+
import org.springframework.security.core.context.SecurityContextHolder;
37+
import org.springframework.security.core.context.SecurityContextImpl;
3238
import org.springframework.security.core.userdetails.UserDetailsService;
3339
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
3440
import org.springframework.security.web.FilterChainProxy;
3541
import org.springframework.security.web.SecurityFilterChain;
3642
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
43+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
44+
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
45+
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
3746
import org.springframework.test.web.servlet.MockMvc;
3847

3948
import static org.assertj.core.api.Assertions.assertThat;
4049
import static org.hamcrest.Matchers.containsString;
50+
import static org.mockito.ArgumentMatchers.any;
51+
import static org.mockito.BDDMockito.given;
52+
import static org.mockito.BDDMockito.willAnswer;
53+
import static org.mockito.Mockito.mock;
4154
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
55+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
4256
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
4357
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
4458
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -126,6 +140,56 @@ public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJa
126140
this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound());
127141
}
128142

143+
@Test
144+
public void webauthnWhenConfiguredMessageConverter() throws Exception {
145+
TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
146+
SecurityContextHolder.setContext(new SecurityContextImpl(user));
147+
PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions
148+
.createPublicKeyCredentialCreationOptions()
149+
.build();
150+
WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class);
151+
ConfigMessageConverter.rpOperations = rpOperations;
152+
given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options);
153+
HttpMessageConverter<Object> converter = mock(HttpMessageConverter.class);
154+
given(converter.canWrite(any(), any())).willReturn(true);
155+
String expectedBody = "123";
156+
willAnswer((args) -> {
157+
HttpOutputMessage out = (HttpOutputMessage) args.getArguments()[2];
158+
out.getBody().write(expectedBody.getBytes(StandardCharsets.UTF_8));
159+
return null;
160+
}).given(converter).write(any(), any(), any());
161+
ConfigMessageConverter.converter = converter;
162+
this.spring.register(ConfigMessageConverter.class).autowire();
163+
this.mvc.perform(post("/webauthn/register/options"))
164+
.andExpect(status().isOk())
165+
.andExpect(content().string(expectedBody));
166+
}
167+
168+
@Configuration
169+
@EnableWebSecurity
170+
static class ConfigMessageConverter {
171+
172+
private static HttpMessageConverter<Object> converter;
173+
174+
private static WebAuthnRelyingPartyOperations rpOperations;
175+
176+
@Bean
177+
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
178+
return ConfigMessageConverter.rpOperations;
179+
}
180+
181+
@Bean
182+
UserDetailsService userDetailsService() {
183+
return new InMemoryUserDetailsManager();
184+
}
185+
186+
@Bean
187+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
188+
return http.csrf(AbstractHttpConfigurer::disable).webAuthn((c) -> c.messageConverter(converter)).build();
189+
}
190+
191+
}
192+
129193
@Configuration
130194
@EnableWebSecurity
131195
static class DefaultWebauthnConfiguration {

web/src/main/java/org/springframework/security/web/webauthn/registration/PublicKeyCredentialCreationOptionsFilter.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -53,6 +53,8 @@
5353
* {@link PublicKeyCredentialCreationOptions} for <a href=
5454
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
5555
* a new credential.
56+
*
57+
* @author DingHao
5658
*/
5759
public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilter {
5860

@@ -67,7 +69,7 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt
6769

6870
private final WebAuthnRelyingPartyOperations rpOperations;
6971

70-
private final HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
72+
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
7173
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
7274

7375
/**
@@ -103,4 +105,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
103105
this.converter.write(options, MediaType.APPLICATION_JSON, new ServletServerHttpResponse(response));
104106
}
105107

108+
/**
109+
* Set the {@link HttpMessageConverter} to read the
110+
* {@link WebAuthnRegistrationFilter.WebAuthnRegistrationRequest} and write the
111+
* response. The default is {@link MappingJackson2HttpMessageConverter}.
112+
* @param converter the {@link HttpMessageConverter} to use. Cannot be null.
113+
*/
114+
public void setConverter(HttpMessageConverter<Object> converter) {
115+
Assert.notNull(converter, "converter cannot be null");
116+
this.converter = converter;
117+
}
118+
106119
}

0 commit comments

Comments
 (0)