8000 Replace a credential only if unchanged when re-hashing a password by ahus1 · Pull Request #55030 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Replace a credential only if unchanged when re-hashing a password - #55030

Merged
ahus1 merged 5 commits into
keycloak:mainfrom
ahus1:is-55028-atomic-credential-update
Nov 11, 2026
Merged

ahus1 merged 5 commits into
keycloak:mainfrom
ahus1:is-55028-atomic-credential-update

Conversation

@ahus1
@ahus1 ahus1 commented Nov 11, 2026 •
Copy link
Copy Markdown
Member

Closes #55028

Second part of the split of #53742, after #55027.

Decisions and assumptions

  • The new method updateStoredCredential(oldCred, newCred) is a compare-and-swap: it replaces the credential only if the stored one still has the content of oldCred (type, label, created date, secret data, credential data), and returns false otherwise. Comparing the whole credential also means the re-hash doesn't overwrite a concurrent change of the label or created date.
  • The JPA stores lock the row with a pessimistic lock instead of relying on a version column. This needs no DB migration, so it can be backported to 26.6.
  • The new methods on the SPI interfaces have a non-atomic default implementation, so existing implementations keep compiling. The Javadoc says that custom implementations should override them, and the defaults are removed in 27.0 in Remove the non-atomic default implementations of the credential update methods #55034.
  • The refresh after taking the lock is skipped if the transaction already holds the lock, as a refresh then reads with a plain read, which can be stale on MySQL/MariaDB under REPEATABLE READ.
  • If the credential changed, the re-hash is skipped and logged at debug level. The password is re-hashed on the next login.
  • The new tests are in the new test framework, including a case where another transaction commits a change after the credential was loaded, which only passes because of 8000 the refresh after taking the lock. The federated credential store is not covered by a test, as that needs a custom user storage provider. The old re-hash tests in the Arquillian PasswordHashingTest still cover the happy path and are left in place, to be migrated in a separate PR.
  • The migration guide note that custom credential stores should implement the atomic methods, and that the defaults will be removed, is added in the 26.8 backport, as the API is introduced to users there. It is meant for the "Deprecated features" section:
=== Atomic credential update for custom credential stores
// https://github.com/keycloak/keycloak/issues/55028

The new methods `SubjectCredentialManager.updateStoredCredential(oldCred, newCred)`, `UserCredentialStore.updateCredential(realm, user, oldCred, newCred)` and `UserFederatedUserCredentialStore.updateCredential(realm, userId, oldCred, newCred)` replace a credential only if the stored credential still has the content of `oldCred`, and return `false` otherwise.
{project_name} uses them to re-hash a password after a login, so that a password or label that was changed in the meantime is not overwritten.

Custom implementations of these interfaces should override the new methods, and perform the comparison and the update atomically, for example by locking the stored credential.
The default implementations exist only for backwards compatibility.
They check and update in two separate steps, so a concurrent change can still be lost.
The default implementations will be removed in a future release, at which point custom implementations that do not provide the methods will no longer compile.

Based on the work of @d53dave in #53742.

Add updateStoredCredential(oldCred, newCred) that replaces a stored credential only if it still has the content of the old version, using a pessimistic lock in the JPA stores. Use it in the deferred password re-hash, so that it no longer overwrites a credential that changed after it was validated.

Closes keycloak#55028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The tests do not cover a stale persistence context after a concurrent password change, leaving rejection of the conditional update and preservation of the newer password unverified.

1 open issue
What changed in this PR

This PR addresses #55028 by making deferred password re-hashing conditional on the credential remaining unchanged, protecting intervening password and metadata updates.

Changes:

  • Adds conditional-update APIs and credential-content comparison, with compatibility defaults for existing implementations.
  • Implements locking in both JPA credential stores and routes conditional updates through credential managers and cache invalidation.
  • Adds regression tests for intervening password changes, unchanged-password re-hashing, and conditional replacement.
File Description
tests/​base/​src/​test/​java/​org/​keycloak/​tests/​model/​PasswordRehashTest.java Tests re-hashing and conditional credential replacement.
services/​src/​main/​java/​org/​keycloak/​credential/​PasswordCredentialProvider.java Uses conditional replacement for deferred re-hashing.
server-spi/​src/​main/​java/​org/​keycloak/​models/​SubjectCredentialManager.java Adds the conditional-update manager API.
server-spi/​src/​main/​java/​org/​keycloak/​credential/​UserCredentialStore.java Adds the conditional-update storage API.
server-spi/​src/​main/​java/​org/​keycloak/​credential/​CredentialModel.java Adds credential-content comparison.
server-spi-private/​src/​main/​java/​org/​keycloak/​models/​light/​EmptyCredentialManager.java Returns false for conditional updates.
model/​storage/​src/​main/​java/​org/​keycloak/​storage/​federated/​UserFederatedUserCredentialStore.java Adds conditional updates to the federated storage API.
model/​storage/​src/​main/​java/​org/​keycloak/​credential/​UserCredentialManager.java Delegates conditional updates to the appropriate store.
model/​jpa/​src/​main/​java/​org/​keycloak/​storage/​jpa/​JpaUserFederatedStorageProvider.java Implements locked conditional updates for federated credentials.
model/​jpa/​src/​main/​java/​org/​keycloak/​models/​jpa/​JpaUserProvider.java Delegates conditional updates to the JPA credential store.
model/​jpa/​src/​main/​java/​org/​keycloak/​models/​jpa/​JpaUserCredentialStore.java Locks and refreshes local credentials before comparison.
model/​infinispan/​src/​main/​java/​org/​keycloak/​models/​cache/​infinispan/​SubjectCredentialManagerCacheAdapter.java Invalidates cached state before conditional updates.

Review effort: Balanced

Closes keycloak#55028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive credential atomicity depends on Hibernate locking and refresh semantics that need database-backed validation, especially under MySQL/MariaDB REPEATABLE READ, while the federated JPA path remains untested.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Closes keycloak#55028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive credential-write atomicity depends on database-specific locking and refresh behavior, requiring maintainer validation across supported databases and the untested federated-store path.

No open issues 🎉

Review effort: Balanced

ahus1 and others added 2 commits November 11, 2026 11:47
Closes keycloak#55028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
… is usable

Closes keycloak#55028

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because concurrent credential deletion can cause an exception and mark the transaction rollback-only instead of returning the documented false.

No open issues 🎉

Review effort: Balanced

@mabartos
Copy link
Copy Markdown
Member

@ahus1 I've just briefly reviewed and it seems that the approach could also be used to resolve the: #54414

And is there any specific reason to use pessimistic locking instead of optimistic? As we have the version column on the entity, there should not be a problem, right? Maybe for federated credentials?

@ahus1
ahus1 commented Nov 11, 2026
Copy link
Copy Markdown
Member Author

And is there any specific reason to use pessimistic locking instead of optimistic? As we have the version column on the entity, there should not be a problem, right? Maybe for federated credentials?

@mabartos - IMHO optimistic locking would not work here. By default, it works only if someone modifies the row concurrently between reading and writing it within the same transaction.

Here, we're reading in one transaction (the password check), and later writing in another transaction (the rehashing). We could make it work by memorizing the version we saw in the first transaction and then compare it in the second transaction. For example, by encoding it the credential data.

But this is something we don't do anywhere (yet) in Keycloak, so this is the approach I've chosen. If we would forward the version ID to the caller (for example in the Admin UI), we would also be able to recognize if someone has changed the data in the meantime. Again, nothing we do today, and outside of the scope.

So I'd like to proceed with the PR as is.

@mabartos
Copy link
Copy Markdown
Member

@mabartos - IMHO optimistic locking would not work here. By default, it works only if someone modifies the row concurrently between reading and writing it within the same transaction.
Here, we're reading in one transaction (the password check), and later writing in another transaction (the rehashing). We could make it work by memorizing the version we saw in the first transaction and then compare it in the second transaction. For example, by encoding it the credential data.
But this is something we don't do anywhere (yet) in Keycloak, so this is the approach I've chosen. If we would forward the version ID to the caller (for example in the Admin UI), we would also be able to recognize if someone has changed the data in the meantime. Again, nothing we do today, and outside of the scope.
So I'd like to proceed with the PR as is.

@ahus1 Thanks for the further explanation. Yep, let's stick with the common way we do in Keycloak, as pessimistic locking should not cause any problem for this.

@ahus1
ahus1 commented Nov 11, 2026
Copy link
Copy Markdown
Member Author

Can someone from the @keycloak/core-iam team help out in reviewing the JPA parts of this PR? Once that's in, it should be ready for merging. Thanks!

@ahus1
ahus1 merged commit e4bcd40 into keycloak:main Nov 11, 2026
184 of 190 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deferred password re-hash overwrites a credential that changed after it was validated

4 participants

0