mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-03-01 02:49:11 +00:00
WebAuthnConfigurer Supports HttpMessageConverter
Closes gh-16397
This commit is contained in:
commit
4dc1dcbf24
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
@ -63,6 +64,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
|
|
||||||
private boolean disableDefaultRegistrationPage = false;
|
private boolean disableDefaultRegistrationPage = false;
|
||||||
|
|
||||||
|
private HttpMessageConverter<Object> converter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Relying Party id.
|
* The Relying Party id.
|
||||||
* @param rpId the relying party id
|
* @param rpId the relying party id
|
||||||
@ -116,6 +119,17 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
return this;
|
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
|
@Override
|
||||||
public void configure(H http) throws Exception {
|
public void configure(H http) throws Exception {
|
||||||
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
|
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
|
||||||
@ -130,9 +144,17 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
|
|||||||
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
|
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
|
||||||
webAuthnAuthnFilter.setAuthenticationManager(
|
webAuthnAuthnFilter.setAuthenticationManager(
|
||||||
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
|
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.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
|
||||||
http.addFilterAfter(new WebAuthnRegistrationFilter(userCredentials, rpOperations), AuthorizationFilter.class);
|
http.addFilterAfter(webAuthnRegistrationFilter, AuthorizationFilter.class);
|
||||||
http.addFilterBefore(new PublicKeyCredentialCreationOptionsFilter(rpOperations), AuthorizationFilter.class);
|
http.addFilterBefore(creationOptionsFilter, AuthorizationFilter.class);
|
||||||
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
|
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
|
||||||
|
|
||||||
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
|
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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;
|
package org.springframework.security.config.annotation.web.configurers;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.test.SpringTestContext;
|
import org.springframework.security.config.test.SpringTestContext;
|
||||||
import org.springframework.security.config.test.SpringTestContextExtension;
|
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.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||||
import org.springframework.security.web.FilterChainProxy;
|
import org.springframework.security.web.FilterChainProxy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
|
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 org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
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.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.content;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
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());
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
static class DefaultWebauthnConfiguration {
|
static class DefaultWebauthnConfiguration {
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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=
|
* {@link PublicKeyCredentialCreationOptions} for <a href=
|
||||||
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
|
* "https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create">creating</a>
|
||||||
* a new credential.
|
* a new credential.
|
||||||
|
*
|
||||||
|
* @author DingHao
|
||||||
*/
|
*/
|
||||||
public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilter {
|
public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ public class PublicKeyCredentialCreationOptionsFilter extends OncePerRequestFilt
|
|||||||
|
|
||||||
private final WebAuthnRelyingPartyOperations rpOperations;
|
private final WebAuthnRelyingPartyOperations rpOperations;
|
||||||
|
|
||||||
private final HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
private HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(
|
||||||
Jackson2ObjectMapperBuilder.json().modules(new WebauthnJackson2Module()).build());
|
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));
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user