Skip to content

MapUserCredentialRepository leaves a stale owner index when a credential ID is reassigned #19847

Description

@789-yu

Describe the bug

MapUserCredentialRepository.save does not remove an existing credential ID
from its previous owner's index when the same credential ID is saved with a
different userEntityUserId.

This leaves the primary credential index and the owner-to-credential-ID index
inconsistent. After deleting the credential, the old owner's index still
contains the credential ID. A subsequent findByUserId call then throws a
NullPointerException.

This is not reproducible through the standard /webauthn/register browser
registration flow because
Webauthn4JRelyingPartyOperations.registerCredential checks whether the
credential ID already exists first.

It is reproducible when application code directly calls
MapUserCredentialRepository.save with an existing credential ID and a
different owner ID.

To Reproduce

Sample repository:

https://github.com/789-yu/spring-security/tree/repro-map-user-credential-repository

Reproducer test:

https://github.com/789-yu/spring-security/blob/repro-map-user-credential-repository/webauthn/src/test/java/org/springframework/security/web/webauthn/management/MapUserCredentialRepositoryReproductionTests.java

Environment:

  • Spring Security commit: 29f8dd53cd
  • Version: 7.1.0-243-g29f8dd53cd
  • JDK: 21
  • Module: spring-security-webauthn

Run:

./gradlew :spring-security-webauthn:test \
  --tests org.springframework.security.web.webauthn.management.MapUserCredentialRepositoryReproductionTests \
  --no-daemon \
  -PtestToolchain=21 \
  --rerun-tasks \
  --console=plain

The reproducer performs these operations:

1. Save a credential for oldOwnerId.
2. Save the same credential ID again with newOwnerId.
3. Query the old owner.
4. Delete the credential by ID.
5. Query the old owner again.

Observed output:

1 old owner records after overwrite: [ImmutableCredentialRecord@...]
2 main index lookup from old owner credentialId: ImmutableCredentialRecord@...
3 new owner records after delete: []
4 old owner lookup after delete: java.lang.NullPointerException

The old owner's index still contains the credential ID after reassignment.
The primary index points to the new owner's record. Deleting the credential only
removes it from the new owner's index. The old owner's stale ID remains, and
findByUserId(oldOwnerId) throws a NullPointerException.

Expected behavior

When an existing credential ID is saved with a different owner, both internal
indexes should remain consistent.

The credential ID should either:

- be removed from the previous owner's index before being added to the new
  owner's index; or

- be rejected if reassignment is not supported.

After deleting a credential, querying either owner should not leave a stale
credential ID or throw a NullPointerException.

**Sample**

A minimal reproducer is available here:

https://github.com/789-yu/spring-security/tree/repro-map-user-credential-repository

The reproducer test is available here:

https://github.com/789-yu/spring-security/blob/repro-map-user-credential-repository/webauthn/src/test/java/org/springframework/security/web/webauthn/management/MapUserCredentialRepositoryReproductionTests.java

[MapUserCredentialRepositoryReproductionTests.java](https://github.com/user-attachments/files/33189187/MapUserCredentialRepositoryReproductionTests.java)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions