|  | 
| 1 | 1 | /* | 
| 2 |  | - * Copyright 2002-2024 the original author or authors. | 
|  | 2 | + * Copyright 2002-2025 the original author or authors. | 
| 3 | 3 |  * | 
| 4 | 4 |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
| 5 | 5 |  * you may not use this file except in compliance with the License. | 
|  | 
| 16 | 16 | 
 | 
| 17 | 17 | package org.springframework.security.config.annotation.web.configurers; | 
| 18 | 18 | 
 | 
|  | 19 | +import java.io.IOException; | 
| 19 | 20 | import java.util.List; | 
| 20 | 21 | 
 | 
| 21 | 22 | import org.junit.jupiter.api.Test; | 
|  | 
| 24 | 25 | import org.springframework.beans.factory.annotation.Autowired; | 
| 25 | 26 | import org.springframework.context.annotation.Bean; | 
| 26 | 27 | import org.springframework.context.annotation.Configuration; | 
|  | 28 | +import org.springframework.http.HttpInputMessage; | 
|  | 29 | +import org.springframework.http.HttpOutputMessage; | 
|  | 30 | +import org.springframework.http.converter.AbstractHttpMessageConverter; | 
|  | 31 | +import org.springframework.http.converter.HttpMessageConverter; | 
|  | 32 | +import org.springframework.http.converter.HttpMessageNotReadableException; | 
|  | 33 | +import org.springframework.http.converter.HttpMessageNotWritableException; | 
|  | 34 | +import org.springframework.security.authentication.TestingAuthenticationToken; | 
| 27 | 35 | import org.springframework.security.config.Customizer; | 
| 28 | 36 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; | 
| 29 | 37 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | 
| 30 | 38 | import org.springframework.security.config.test.SpringTestContext; | 
| 31 | 39 | import org.springframework.security.config.test.SpringTestContextExtension; | 
|  | 40 | +import org.springframework.security.core.context.SecurityContextHolder; | 
|  | 41 | +import org.springframework.security.core.context.SecurityContextImpl; | 
| 32 | 42 | import org.springframework.security.core.userdetails.UserDetailsService; | 
| 33 | 43 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; | 
| 34 | 44 | import org.springframework.security.web.FilterChainProxy; | 
| 35 | 45 | import org.springframework.security.web.SecurityFilterChain; | 
| 36 | 46 | import org.springframework.security.web.authentication.ui.DefaultResourcesFilter; | 
|  | 47 | +import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions; | 
|  | 48 | +import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions; | 
|  | 49 | +import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations; | 
| 37 | 50 | import org.springframework.test.web.servlet.MockMvc; | 
| 38 | 51 | 
 | 
| 39 | 52 | import static org.assertj.core.api.Assertions.assertThat; | 
| 40 | 53 | import static org.hamcrest.Matchers.containsString; | 
|  | 54 | +import static org.mockito.ArgumentMatchers.any; | 
|  | 55 | +import static org.mockito.BDDMockito.given; | 
|  | 56 | +import static org.mockito.Mockito.mock; | 
| 41 | 57 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | 
|  | 58 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | 
| 42 | 59 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; | 
| 43 | 60 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | 
| 44 | 61 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | 
| @@ -126,6 +143,66 @@ public void webauthnWhenConfiguredAndNoDefaultRegistrationPageThenDoesNotServeJa | 
| 126 | 143 | 		this.mvc.perform(get("/login/webauthn.js")).andExpect(status().isNotFound()); | 
| 127 | 144 | 	} | 
| 128 | 145 | 
 | 
|  | 146 | +	@Test | 
|  | 147 | +	public void webauthnWhenConfiguredMessageConverter() throws Exception { | 
|  | 148 | +		TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); | 
|  | 149 | +		SecurityContextHolder.setContext(new SecurityContextImpl(user)); | 
|  | 150 | +		PublicKeyCredentialCreationOptions options = TestPublicKeyCredentialCreationOptions | 
|  | 151 | +			.createPublicKeyCredentialCreationOptions() | 
|  | 152 | +			.build(); | 
|  | 153 | +		WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class); | 
|  | 154 | +		ConfigMessageConverter.rpOperations = rpOperations; | 
|  | 155 | +		given(rpOperations.createPublicKeyCredentialCreationOptions(any())).willReturn(options); | 
|  | 156 | +		HttpMessageConverter<Object> converter = new AbstractHttpMessageConverter<>() { | 
|  | 157 | +			@Override | 
|  | 158 | +			protected boolean supports(Class<?> clazz) { | 
|  | 159 | +				return true; | 
|  | 160 | +			} | 
|  | 161 | + | 
|  | 162 | +			@Override | 
|  | 163 | +			protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) | 
|  | 164 | +					throws IOException, HttpMessageNotReadableException { | 
|  | 165 | +				return null; | 
|  | 166 | +			} | 
|  | 167 | + | 
|  | 168 | +			@Override | 
|  | 169 | +			protected void writeInternal(Object o, HttpOutputMessage outputMessage) | 
|  | 170 | +					throws IOException, HttpMessageNotWritableException { | 
|  | 171 | +				outputMessage.getBody().write("123".getBytes()); | 
|  | 172 | +			} | 
|  | 173 | +		}; | 
|  | 174 | +		ConfigMessageConverter.converter = converter; | 
|  | 175 | +		this.spring.register(ConfigMessageConverter.class).autowire(); | 
|  | 176 | +		this.mvc.perform(post("/webauthn/register/options")) | 
|  | 177 | +			.andExpect(status().isOk()) | 
|  | 178 | +			.andExpect(content().string("123")); | 
|  | 179 | +	} | 
|  | 180 | + | 
|  | 181 | +	@Configuration | 
|  | 182 | +	@EnableWebSecurity | 
|  | 183 | +	static class ConfigMessageConverter { | 
|  | 184 | + | 
|  | 185 | +		private static HttpMessageConverter<Object> converter; | 
|  | 186 | + | 
|  | 187 | +		private static WebAuthnRelyingPartyOperations rpOperations; | 
|  | 188 | + | 
|  | 189 | +		@Bean | 
|  | 190 | +		WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() { | 
|  | 191 | +			return ConfigMessageConverter.rpOperations; | 
|  | 192 | +		} | 
|  | 193 | + | 
|  | 194 | +		@Bean | 
|  | 195 | +		UserDetailsService userDetailsService() { | 
|  | 196 | +			return new InMemoryUserDetailsManager(); | 
|  | 197 | +		} | 
|  | 198 | + | 
|  | 199 | +		@Bean | 
|  | 200 | +		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | 
|  | 201 | +			return http.csrf(AbstractHttpConfigurer::disable).webAuthn((c) -> c.messageConverter(converter)).build(); | 
|  | 202 | +		} | 
|  | 203 | + | 
|  | 204 | +	} | 
|  | 205 | + | 
| 129 | 206 | 	@Configuration | 
| 130 | 207 | 	@EnableWebSecurity | 
| 131 | 208 | 	static class DefaultWebauthnConfiguration { | 
|  | 
0 commit comments