Skip to content

Commit

Permalink
WebAuthnConfigurer Supports HttpMessageConverter
Browse files Browse the repository at this point in the history
Closes gh-16397
  • Loading branch information
rwinch committed Jan 18, 2025
2 parents 4fc99aa + 5462b4c commit 4dc1dcb
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,7 @@

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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -116,6 +119,17 @@ public WebAuthnConfigurer<H> disableDefaultRegistrationPage(boolean disable) {
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(() -> {
Expand All @@ -130,9 +144,17 @@ public void configure(H http) throws Exception {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -24,21 +25,34 @@
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;
Expand Down Expand Up @@ -126,6 +140,56 @@ public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJa
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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -53,6 +53,8 @@
* {@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 {

Expand All @@ -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());

/**
Expand Down Expand Up @@ -103,4 +105,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
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;
}

}

0 comments on commit 4dc1dcb

Please sign in to comment.