From 858abcbdf89653e11d6a158a0ae1e57c9c60bbc3 Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Thu, 2 Jul 2026 22:17:30 +0300 Subject: [PATCH] Allow custom AuthenticationConverter in WebAuthnAuthenticationFilter This change allows us to delegate the `attemptAuthentication` logic to the `AuthenticationConverter` interface and enables the definition of custom instances. Additionally, we have added a default implementation, `WebAuthnAuthenticationConverter`, whose behavior is fully deterministic with respect to the previous implementation. Closes: gh-19411 Signed-off-by: Andrey Litvitski --- .../WebAuthnAuthenticationConverter.java | 110 ++++++++++++++++++ .../WebAuthnAuthenticationFilter.java | 55 ++++----- 2 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationConverter.java diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationConverter.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationConverter.java new file mode 100644 index 00000000000..9214f9bf2ae --- /dev/null +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationConverter.java @@ -0,0 +1,110 @@ +/* + * Copyright 2004-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.webauthn.authentication; + +import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.ResolvableType; +import org.springframework.http.converter.SmartHttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse; +import org.springframework.security.web.webauthn.api.PublicKeyCredential; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; +import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; +import org.springframework.util.Assert; + +/** + * An {@link AuthenticationConverter} that generates a + * {@link WebAuthnAuthenticationRequestToken} from a WebAuthn authentication request. + * + * @author Andrey Litvitski + * @since 7.1.0 + */ +public class WebAuthnAuthenticationConverter implements AuthenticationConverter { + + private SmartHttpMessageConverter converter; + + private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository; + + /** + * Constructs a {@link WebAuthnAuthenticationConverter} given a strategy for reading + * the {@code PublicKeyCredential} and loading the + * {@code PublicKeyCredentialRequestOptions}. + * @param converter the strategy for reading the {@code PublicKeyCredential} from the + * request + * @param requestOptionsRepository the strategy for loading the + * {@code PublicKeyCredentialRequestOptions} + */ + public WebAuthnAuthenticationConverter(SmartHttpMessageConverter converter, + PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) { + Assert.notNull(converter, "converter cannot be null"); + Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null"); + this.converter = converter; + this.requestOptionsRepository = requestOptionsRepository; + } + + @Override + public @Nullable WebAuthnAuthenticationRequestToken convert(HttpServletRequest request) { + ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request); + ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class, + AuthenticatorAssertionResponse.class); + PublicKeyCredential publicKeyCredential; + try { + publicKeyCredential = (PublicKeyCredential) this.converter + .read(resolvableType, httpRequest, null); + } + catch (Exception ex) { + throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex); + } + PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request); + if (requestOptions == null) { + throw new BadCredentialsException( + "Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found."); + } + RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions, + publicKeyCredential); + return new WebAuthnAuthenticationRequestToken(authenticationRequest); + } + + /** + * Sets the {@link SmartHttpMessageConverter} to use for reading + * {@code PublicKeyCredential} from the request. The + * default is {@link JacksonJsonHttpMessageConverter}. + * @param converter the {@link SmartHttpMessageConverter} to use. Cannot be null. + * @since 7.0 + */ + public void setConverter(SmartHttpMessageConverter converter) { + Assert.notNull(converter, "converter cannot be null"); + this.converter = converter; + } + + /** + * Sets the {@link PublicKeyCredentialRequestOptionsRepository} to use. The default is + * {@link HttpSessionPublicKeyCredentialRequestOptionsRepository}. + * @param requestOptionsRepository the + * {@link PublicKeyCredentialRequestOptionsRepository} to use. Cannot be null. + */ + public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) { + Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null"); + this.requestOptionsRepository = requestOptionsRepository; + } + +} diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java index a1ae19f9205..f7449bec2de 100644 --- a/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationFilter.java @@ -38,20 +38,15 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.SmartHttpMessageConverter; import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationConverter; import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler; import org.springframework.security.web.authentication.HttpMessageConverterAuthenticationSuccessHandler; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -import org.springframework.security.web.webauthn.api.AuthenticatorAssertionResponse; -import org.springframework.security.web.webauthn.api.PublicKeyCredential; -import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; import org.springframework.security.web.webauthn.jackson.WebauthnJacksonModule; -import org.springframework.security.web.webauthn.management.RelyingPartyAuthenticationRequest; import org.springframework.util.Assert; import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.pathPattern; @@ -78,6 +73,7 @@ * * * @author Rob Winch + * @author Andrey Litvitski * @since 6.4 */ public class WebAuthnAuthenticationFilter extends AbstractAuthenticationProcessingFilter { @@ -87,6 +83,11 @@ public class WebAuthnAuthenticationFilter extends AbstractAuthenticationProcessi private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository(); + private final WebAuthnAuthenticationConverter webAuthnAuthenticationConverter = new WebAuthnAuthenticationConverter( + this.converter, this.requestOptionsRepository); + + private AuthenticationConverter authenticationConverter = this.webAuthnAuthenticationConverter; + public WebAuthnAuthenticationFilter() { super(pathPattern(HttpMethod.POST, "/login/webauthn")); setSecurityContextRepository(new HttpSessionSecurityContextRepository()); @@ -96,29 +97,14 @@ public WebAuthnAuthenticationFilter() { } @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + public @Nullable Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { - ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(request); - ResolvableType resolvableType = ResolvableType.forClassWithGenerics(PublicKeyCredential.class, - AuthenticatorAssertionResponse.class); - PublicKeyCredential publicKeyCredential = null; - try { - publicKeyCredential = (PublicKeyCredential) this.converter - .read(resolvableType, httpRequest, null); - } - catch (Exception ex) { - throw new BadCredentialsException("Unable to authenticate the PublicKeyCredential", ex); - } - PublicKeyCredentialRequestOptions requestOptions = this.requestOptionsRepository.load(request); - if (requestOptions == null) { - throw new BadCredentialsException( - "Unable to authenticate the PublicKeyCredential. No PublicKeyCredentialRequestOptions found."); + Authentication authentication = this.authenticationConverter.convert(request); + if (authentication == null) { + return null; } this.requestOptionsRepository.save(request, response, null); - RelyingPartyAuthenticationRequest authenticationRequest = new RelyingPartyAuthenticationRequest(requestOptions, - publicKeyCredential); - WebAuthnAuthenticationRequestToken token = new WebAuthnAuthenticationRequestToken(authenticationRequest); - return getAuthenticationManager().authenticate(token); + return getAuthenticationManager().authenticate(authentication); } /** @@ -132,6 +118,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ public void setConverter(GenericHttpMessageConverter converter) { Assert.notNull(converter, "converter cannot be null"); this.converter = new GenericHttpMessageConverterAdapter<>(converter); + this.webAuthnAuthenticationConverter.setConverter(this.converter); } /** @@ -144,6 +131,21 @@ public void setConverter(GenericHttpMessageConverter converter) { public void setConverter(SmartHttpMessageConverter converter) { Assert.notNull(converter, "converter cannot be null"); this.converter = converter; + this.webAuthnAuthenticationConverter.setConverter(converter); + } + + /** + * Sets the {@link AuthenticationConverter} to use for converting the + * {@link HttpServletRequest} into an {@link Authentication}. + * @param authenticationConverter the {@link AuthenticationConverter} to use. Cannot + * be null. + * @since 7.1 + */ + @Override + public void setAuthenticationConverter(AuthenticationConverter authenticationConverter) { + Assert.notNull(authenticationConverter, "authenticationConverter cannot be null"); + this.authenticationConverter = authenticationConverter; + super.setAuthenticationConverter(authenticationConverter); } /** @@ -155,6 +157,7 @@ public void setConverter(SmartHttpMessageConverter converter) { public void setRequestOptionsRepository(PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) { Assert.notNull(requestOptionsRepository, "requestOptionsRepository cannot be null"); this.requestOptionsRepository = requestOptionsRepository; + this.webAuthnAuthenticationConverter.setRequestOptionsRepository(requestOptionsRepository); } /**