Merge branch '6.4.x'

- Fix WebAuthn saves Anonymous PublicKeyCredentialUserEntity

Closes gh-16821
This commit is contained in:
Rob Winch 2025-03-25 16:19:14 -05:00
commit 491d28b6bb
No known key found for this signature in database
2 changed files with 61 additions and 5 deletions

View File

@ -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.
@ -333,9 +333,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
public PublicKeyCredentialRequestOptions createCredentialRequestOptions( public PublicKeyCredentialRequestOptions createCredentialRequestOptions(
PublicKeyCredentialRequestOptionsRequest request) { PublicKeyCredentialRequestOptionsRequest request) {
Authentication authentication = request.getAuthentication(); Authentication authentication = request.getAuthentication();
// FIXME: do not load credentialRecords if anonymous List<CredentialRecord> credentialRecords = findCredentialRecords(authentication);
PublicKeyCredentialUserEntity userEntity = findUserEntityOrCreateAndSave(authentication.getName());
List<CredentialRecord> credentialRecords = this.userCredentials.findByUserId(userEntity.getId());
return PublicKeyCredentialRequestOptions.builder() return PublicKeyCredentialRequestOptions.builder()
.allowCredentials(credentialDescriptors(credentialRecords)) .allowCredentials(credentialDescriptors(credentialRecords))
.challenge(Bytes.random()) .challenge(Bytes.random())
@ -346,6 +344,17 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
.build(); .build();
} }
private List<CredentialRecord> findCredentialRecords(Authentication authentication) {
if (!this.trustResolver.isAuthenticated(authentication)) {
return Collections.emptyList();
}
PublicKeyCredentialUserEntity userEntity = this.userEntities.findByUsername(authentication.getName());
if (userEntity == null) {
return Collections.emptyList();
}
return this.userCredentials.findByUserId(userEntity.getId());
}
@Override @Override
public PublicKeyCredentialUserEntity authenticate(RelyingPartyAuthenticationRequest request) { public PublicKeyCredentialUserEntity authenticate(RelyingPartyAuthenticationRequest request) {
PublicKeyCredentialRequestOptions requestOptions = request.getRequestOptions(); PublicKeyCredentialRequestOptions requestOptions = request.getRequestOptions();

View File

@ -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.
@ -42,6 +42,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.PasswordEncodedUser;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse; import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse;
import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder; import org.springframework.security.web.webauthn.api.AuthenticatorAttestationResponse.AuthenticatorAttestationResponseBuilder;
import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria; import org.springframework.security.web.webauthn.api.AuthenticatorSelectionCriteria;
@ -66,6 +68,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.assertj.core.api.Assertions.assertThatRuntimeException;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verifyNoInteractions;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class Webauthn4jRelyingPartyOperationsTests { class Webauthn4jRelyingPartyOperationsTests {
@ -536,6 +539,50 @@ class Webauthn4jRelyingPartyOperationsTests {
.isEqualTo(creationOptions.getAuthenticatorSelection().getUserVerification()); .isEqualTo(creationOptions.getAuthenticatorSelection().getUserVerification());
} }
@Test
void createCredentialRequestOptionsWhenAnonymousAuthentication() {
AnonymousAuthenticationToken authentication = new AnonymousAuthenticationToken("key", "anonymousUser",
Set.of(() -> "ROLE_ANONYMOUS"));
PublicKeyCredentialRequestOptionsRequest createRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
authentication);
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOperations
.createCredentialRequestOptions(createRequest);
assertThat(credentialRequestOptions.getAllowCredentials()).isEmpty();
// verify anonymous user not saved
verifyNoInteractions(this.userEntities);
}
@Test
void createCredentialRequestOptionsWhenNullAuthentication() {
PublicKeyCredentialRequestOptionsRequest createRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
null);
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOperations
.createCredentialRequestOptions(createRequest);
assertThat(credentialRequestOptions.getAllowCredentials()).isEmpty();
// verify anonymous user not saved
verifyNoInteractions(this.userEntities);
}
@Test
void createCredentialRequestOptionsWhenAuthenticated() {
UserDetails user = PasswordEncodedUser.user();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null,
user.getAuthorities());
PublicKeyCredentialUserEntity userEntity = TestPublicKeyCredentialUserEntity.userEntity().build();
CredentialRecord credentialRecord = TestCredentialRecord.userCredential().build();
given(this.userEntities.findByUsername(user.getUsername())).willReturn(userEntity);
given(this.userCredentials.findByUserId(userEntity.getId())).willReturn(Arrays.asList(credentialRecord));
PublicKeyCredentialRequestOptionsRequest createRequest = new ImmutablePublicKeyCredentialRequestOptionsRequest(
auth);
PublicKeyCredentialRequestOptions credentialRequestOptions = this.rpOperations
.createCredentialRequestOptions(createRequest);
assertThat(credentialRequestOptions.getAllowCredentials()).extracting(PublicKeyCredentialDescriptor::getId)
.containsExactly(credentialRecord.getCredentialId());
}
private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception { private static AuthenticatorAttestationResponse setFlag(byte... flags) throws Exception {
AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponse AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponse
.createAuthenticatorAttestationResponse(); .createAuthenticatorAttestationResponse();