Browse Source

Fix WebAuthn saves Anonymous PublicKeyCredentialUserEntity

Closes gh-16606
Rob Winch 5 months ago
parent
commit
1f3dd53bdf

+ 13 - 4
web/src/main/java/org/springframework/security/web/webauthn/management/Webauthn4JRelyingPartyOperations.java

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

+ 48 - 1
web/src/test/java/org/springframework/security/web/webauthn/management/Webauthn4jRelyingPartyOperationsTests.java

@@ -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.
@@ -42,6 +42,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 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.AuthenticatorAttestationResponseBuilder;
 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.assertThatRuntimeException;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 @ExtendWith(MockitoExtension.class)
 class Webauthn4jRelyingPartyOperationsTests {
@@ -536,6 +539,50 @@ class Webauthn4jRelyingPartyOperationsTests {
 			.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 {
 		AuthenticatorAttestationResponseBuilder authAttResponseBldr = TestAuthenticatorAttestationResponse
 			.createAuthenticatorAttestationResponse();