Skip to content

Commit baf815f

Browse files
committedApr 3, 2025
Allow Custom PublicKeyCredentialRequestOptionsRepository in WebAuthnConfigurer
Closes spring-projectsgh-16874
1 parent 91b0936 commit baf815f

File tree

2 files changed

+168
-4
lines changed

2 files changed

+168
-4
lines changed
 

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

+31-4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.security.web.csrf.CsrfToken;
3535
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRpEntity;
3636
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsFilter;
37+
import org.springframework.security.web.webauthn.authentication.PublicKeyCredentialRequestOptionsRepository;
3738
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationFilter;
3839
import org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationProvider;
3940
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository;
@@ -67,6 +68,8 @@ public class WebAuthnConfigurer<H extends HttpSecurityBuilder<H>>
6768

6869
private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository;
6970

71+
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository;
72+
7073
private HttpMessageConverter<Object> converter;
7174

7275
/**
@@ -144,37 +147,53 @@ public WebAuthnConfigurer<H> creationOptionsRepository(
144147
return this;
145148
}
146149

150+
/**
151+
* Sets PublicKeyCredentialRequestOptionsRepository.
152+
* @param requestOptionsRepository the requestOptionsRepository
153+
* @return the {@link WebAuthnConfigurer} for further customization
154+
*/
155+
public WebAuthnConfigurer<H> requestOptionsRepository(
156+
PublicKeyCredentialRequestOptionsRepository requestOptionsRepository) {
157+
this.requestOptionsRepository = requestOptionsRepository;
158+
return this;
159+
}
160+
147161
@Override
148162
public void configure(H http) throws Exception {
149-
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class).orElseGet(() -> {
150-
throw new IllegalStateException("Missing UserDetailsService Bean");
151-
});
163+
UserDetailsService userDetailsService = getSharedOrBean(http, UserDetailsService.class)
164+
.orElseThrow(() -> new IllegalStateException("Missing UserDetailsService Bean"));
152165
PublicKeyCredentialUserEntityRepository userEntities = getSharedOrBean(http,
153166
PublicKeyCredentialUserEntityRepository.class)
154167
.orElse(userEntityRepository());
155168
UserCredentialRepository userCredentials = getSharedOrBean(http, UserCredentialRepository.class)
156169
.orElse(userCredentialRepository());
157170
WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials);
158171
PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository();
172+
PublicKeyCredentialRequestOptionsRepository requestOptionsRepository = requestOptionsRepository();
159173
WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter();
160174
webAuthnAuthnFilter.setAuthenticationManager(
161175
new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)));
162176
WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials,
163177
rpOperations);
164178
PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter(
165179
rpOperations);
180+
PublicKeyCredentialRequestOptionsFilter credentialRequestOptionsFilter = new PublicKeyCredentialRequestOptionsFilter(rpOperations);
166181
if (creationOptionsRepository != null) {
167182
webAuthnRegistrationFilter.setCreationOptionsRepository(creationOptionsRepository);
168183
creationOptionsFilter.setCreationOptionsRepository(creationOptionsRepository);
169184
}
185+
if (requestOptionsRepository != null) {
186+
credentialRequestOptionsFilter.setRequestOptionsRepository(requestOptionsRepository);
187+
webAuthnAuthnFilter.setRequestOptionsRepository(requestOptionsRepository);
188+
}
170189
if (this.converter != null) {
171190
webAuthnRegistrationFilter.setConverter(this.converter);
172191
creationOptionsFilter.setConverter(this.converter);
173192
}
174193
http.addFilterBefore(webAuthnAuthnFilter, BasicAuthenticationFilter.class);
175194
http.addFilterAfter(webAuthnRegistrationFilter, AuthorizationFilter.class);
176195
http.addFilterBefore(creationOptionsFilter, AuthorizationFilter.class);
177-
http.addFilterBefore(new PublicKeyCredentialRequestOptionsFilter(rpOperations), AuthorizationFilter.class);
196+
http.addFilterBefore(credentialRequestOptionsFilter, AuthorizationFilter.class);
178197

179198
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
180199
.getSharedObject(DefaultLoginPageGeneratingFilter.class);
@@ -208,6 +227,14 @@ private PublicKeyCredentialCreationOptionsRepository creationOptionsRepository()
208227
return context.getBeanProvider(PublicKeyCredentialCreationOptionsRepository.class).getIfUnique();
209228
}
210229

230+
private PublicKeyCredentialRequestOptionsRepository requestOptionsRepository() {
231+
if (this.requestOptionsRepository != null) {
232+
return this.requestOptionsRepository;
233+
}
234+
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
235+
return context.getBeanProvider(PublicKeyCredentialRequestOptionsRepository.class).getIfUnique();
236+
}
237+
211238
private <C> Optional<C> getSharedOrBean(H http, Class<C> type) {
212239
C shared = http.getSharedObject(type);
213240
return Optional.ofNullable(shared).or(() -> getBeanOrNull(type));

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

+137
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,22 @@
3333
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
3434
import org.springframework.security.config.test.SpringTestContext;
3535
import org.springframework.security.config.test.SpringTestContextExtension;
36+
import org.springframework.security.core.authority.AuthorityUtils;
3637
import org.springframework.security.core.context.SecurityContextHolder;
3738
import org.springframework.security.core.context.SecurityContextImpl;
39+
import org.springframework.security.core.userdetails.User;
3840
import org.springframework.security.core.userdetails.UserDetailsService;
3941
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
4042
import org.springframework.security.web.FilterChainProxy;
4143
import org.springframework.security.web.SecurityFilterChain;
4244
import org.springframework.security.web.authentication.ui.DefaultResourcesFilter;
4345
import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions;
46+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions;
47+
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
4448
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialCreationOptions;
49+
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialRequestOptions;
50+
import org.springframework.security.web.webauthn.api.TestPublicKeyCredentialUserEntity;
51+
import org.springframework.security.web.webauthn.authentication.HttpSessionPublicKeyCredentialRequestOptionsRepository;
4552
import org.springframework.security.web.webauthn.management.WebAuthnRelyingPartyOperations;
4653
import org.springframework.security.web.webauthn.registration.HttpSessionPublicKeyCredentialCreationOptionsRepository;
4754
import org.springframework.test.web.servlet.MockMvc;
@@ -67,6 +74,21 @@ public class WebAuthnConfigurerTests {
6774

6875
public final SpringTestContext spring = new SpringTestContext(this);
6976

77+
private static final String WEBAUTHN_LOGIN_BODY = """
78+
{
79+
"id": "dYF7EGnRFFIXkpXi9XU2wg",
80+
"rawId": "dYF7EGnRFFIXkpXi9XU2wg",
81+
"response": {
82+
"authenticatorData": "y9GqwTRaMpzVDbXq1dyEAXVOxrou08k22ggRC45MKNgdAAAAAA",
83+
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRFVsRzRDbU9naWhKMG1vdXZFcE9HdUk0ZVJ6MGRRWmxUQmFtbjdHQ1FTNCIsIm9yaWdpbiI6Imh0dHBzOi8vZXhhbXBsZS5sb2NhbGhvc3Q6ODQ0MyIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
84+
"signature": "MEYCIQCW2BcUkRCAXDmGxwMi78jknenZ7_amWrUJEYoTkweldAIhAMD0EMp1rw2GfwhdrsFIeDsL7tfOXVPwOtfqJntjAo4z",
85+
"userHandle": "Q3_0Xd64_HW0BlKRAJnVagJTpLKLgARCj8zjugpRnVo"
86+
},
87+
"clientExtensionResults": {},
88+
"authenticatorAttachment": "platform"
89+
}
90+
""";
91+
7092
@Autowired
7193
MockMvc mvc;
7294

@@ -182,6 +204,53 @@ public void webauthnWhenConfiguredPublicKeyCredentialCreationOptionsRepositoryBe
182204
.andExpect(request().sessionAttribute(attrName, options));
183205
}
184206

207+
@Test
208+
public void webauthnWhenConfiguredPublicKeyCredentialRequestOptionsRepositoryBeanPresent() throws Exception {
209+
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create()
210+
.build();
211+
WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class);
212+
ConfigCredentialRequestOptionsRepositoryFromBean.rpOperations = rpOperations;
213+
given(rpOperations.createCredentialRequestOptions(any())).willReturn(options);
214+
String attrName = "attrName";
215+
HttpSessionPublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
216+
requestOptionsRepository.setAttrName(attrName);
217+
ConfigCredentialRequestOptionsRepositoryFromBean.requestOptionsRepository = requestOptionsRepository;
218+
this.spring.register(ConfigCredentialRequestOptionsRepositoryFromBean.class).autowire();
219+
this.mvc.perform(post("/webauthn/authenticate/options"))
220+
.andExpect(status().isOk())
221+
.andExpect(request().sessionAttribute(attrName, options));
222+
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
223+
given(rpOperations.authenticate(any())).willReturn(userEntity);
224+
this.mvc.perform(post("/login/webauthn")
225+
.content(WEBAUTHN_LOGIN_BODY)
226+
.sessionAttr(attrName, options))
227+
.andExpect(status().isOk());
228+
}
229+
230+
@Test
231+
public void webauthnWhenConfiguredPublicKeyCredentialRequestOptionsRepository() throws Exception {
232+
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions
233+
.create()
234+
.build();
235+
WebAuthnRelyingPartyOperations rpOperations = mock(WebAuthnRelyingPartyOperations.class);
236+
ConfigCredentialRequestOptionsRepository.rpOperations = rpOperations;
237+
given(rpOperations.createCredentialRequestOptions(any())).willReturn(options);
238+
String attrName = "attrName";
239+
HttpSessionPublicKeyCredentialRequestOptionsRepository requestOptionsRepository = new HttpSessionPublicKeyCredentialRequestOptionsRepository();
240+
requestOptionsRepository.setAttrName(attrName);
241+
ConfigCredentialRequestOptionsRepository.requestOptionsRepository = requestOptionsRepository;
242+
this.spring.register(ConfigCredentialRequestOptionsRepository.class).autowire();
243+
this.mvc.perform(post("/webauthn/authenticate/options"))
244+
.andExpect(status().isOk())
245+
.andExpect(request().sessionAttribute(attrName, options));
246+
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
247+
given(rpOperations.authenticate(any())).willReturn(userEntity);
248+
this.mvc.perform(post("/login/webauthn")
249+
.content(WEBAUTHN_LOGIN_BODY)
250+
.sessionAttr(attrName, options))
251+
.andExpect(status().isOk());
252+
}
253+
185254
@Test
186255
public void webauthnWhenConfiguredMessageConverter() throws Exception {
187256
TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password", "ROLE_USER");
@@ -264,6 +333,74 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
264333

265334
}
266335

336+
@Configuration
337+
@EnableWebSecurity
338+
static class ConfigCredentialRequestOptionsRepositoryFromBean {
339+
340+
private static HttpSessionPublicKeyCredentialRequestOptionsRepository requestOptionsRepository;
341+
342+
private static WebAuthnRelyingPartyOperations rpOperations;
343+
344+
@Bean
345+
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
346+
return ConfigCredentialRequestOptionsRepositoryFromBean.rpOperations;
347+
}
348+
349+
@Bean
350+
UserDetailsService userDetailsService() {
351+
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
352+
userDetailsService.createUser(User.builder().username("user")
353+
.password("{noop}password")
354+
.authorities(AuthorityUtils.createAuthorityList("ROLE_USER"))
355+
.build());
356+
return userDetailsService;
357+
}
358+
359+
@Bean
360+
HttpSessionPublicKeyCredentialRequestOptionsRepository credentialRequestOptionsRepository() {
361+
return ConfigCredentialRequestOptionsRepositoryFromBean.requestOptionsRepository;
362+
}
363+
364+
@Bean
365+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
366+
return http.csrf(AbstractHttpConfigurer::disable).webAuthn(Customizer.withDefaults()).build();
367+
}
368+
369+
}
370+
371+
@Configuration
372+
@EnableWebSecurity
373+
static class ConfigCredentialRequestOptionsRepository {
374+
375+
private static HttpSessionPublicKeyCredentialRequestOptionsRepository requestOptionsRepository;
376+
377+
private static WebAuthnRelyingPartyOperations rpOperations;
378+
379+
@Bean
380+
WebAuthnRelyingPartyOperations webAuthnRelyingPartyOperations() {
381+
return ConfigCredentialRequestOptionsRepository.rpOperations;
382+
}
383+
384+
@Bean
385+
UserDetailsService userDetailsService() {
386+
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
387+
userDetailsService.createUser(User.builder().username("user")
388+
.password("{noop}password")
389+
.authorities(AuthorityUtils.createAuthorityList("ROLE_USER"))
390+
.build());
391+
return userDetailsService;
392+
}
393+
394+
@Bean
395+
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
396+
return http.csrf(AbstractHttpConfigurer::disable)
397+
.webAuthn((c) -> c.requestOptionsRepository(requestOptionsRepository))
398+
.build();
399+
}
400+
401+
}
402+
403+
267404
@Configuration
268405
@EnableWebSecurity
269406
static class ConfigMessageConverter {

0 commit comments

Comments
 (0)
Please sign in to comment.