fix: prevent NPE in convertDeprecatedCredentialsFormat when credential fields are null - #51557
Conversation
CredentialRepresentation uses boxed Integer for hashIterations, digits, counter, and period, but PasswordCredentialData and OTPCredentialData constructors take primitive int. When these fields are null during user creation via the Admin REST API, auto-unboxing throws a NullPointerException in convertDeprecatedCredentialsFormat. Added null-safe defaults (0) for all nullable Integer fields before passing them to the credential data constructors. Closes keycloak#41640 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Prevents null auto-unboxing failures when converting deprecated credential representations.
Changes:
- Defaults nullable password and OTP integer fields.
- Reformats OTP credential construction.
Suppressed comments (1)
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:292
- A
0period is not usable for TOTP:TimeBasedOTP.Clock#getCurrentInterval()divides by the configured interval, so the first validation attempt throwsArithmeticException. Use the established 30-second default or reject an incomplete TOTP credential.
cred.getPeriod() != null ? cred.getPeriod() : 0,
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| } else if (OTPCredentialModel.TOTP.equals(cred.getType()) || OTPCredentialModel.HOTP.equals(cred.getType())) { | ||
| OTPCredentialData credentialData = new OTPCredentialData(cred.getType(), cred.getDigits(), cred.getCounter(), cred.getPeriod(), cred.getAlgorithm(), null); | ||
| OTPCredentialData credentialData = new OTPCredentialData(cred.getType(), | ||
| cred.getDigits() != null ? cred.getDigits() : 0, |
|
|
||
| if (PasswordCredentialModel.TYPE.equals(cred.getType()) || PasswordCredentialModel.PASSWORD_HISTORY.equals(cred.getType())) { | ||
| PasswordCredentialData credentialData = new PasswordCredentialData(cred.getHashIterations(), cred.getAlgorithm()); | ||
| PasswordCredentialData credentialData = new PasswordCredentialData(cred.getHashIterations() != null ? cred.getHashIterations() : 0, cred.getAlgorithm()); |
Defaulting digits to 0 would produce a trivially guessable OTP ("0")
since HmacOTP computes modulo DIGITS_POWER[0] (= 1). Defaulting period
to 0 would cause ArithmeticException in TimeBasedOTP.Clock due to
division by zero. Use OTPPolicy.DEFAULT_POLICY values (6 digits,
30-second period) instead, matching Keycloak's standard OTP defaults.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (3)
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:284
- The existing
UserCreateTest.createUserWithDeprecatedCredentialsFormatcovers this Admin REST path only whenhashIterationsis present. Add a regression case with the field omitted so the reported null-input behavior and resulting response are verified end to end.
PasswordCredentialData credentialData = new PasswordCredentialData(cred.getHashIterations() != null ? cred.getHashIterations() : 0, cred.getAlgorithm());
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:293
- These fallbacks ignore the target realm's OTP policy, so a realm configured for (for example) 8 digits or a 60-second period will store incompatible 6-digit/30-second credential metadata. Pass the realm into this conversion and use its OTP policy, including
initialCounter, asOTPCredentialModel.createFromPolicydoes.
cred.getDigits() != null ? cred.getDigits() : OTPPolicy.DEFAULT_POLICY.getDigits(),
cred.getCounter() != null ? cred.getCounter() : 0,
cred.getPeriod() != null ? cred.getPeriod() : OTPPolicy.DEFAULT_POLICY.getPeriod(),
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:284
- Defaulting a missing iteration count to
0stores a password credential that PBKDF2 cannot verify:Pbkdf2PasswordHashProviderpasses this value toPBEKeySpec, which rejects non-positive iteration counts. Since the original hash parameters cannot be inferred, reject the malformed deprecated credential with a client validation error rather than persisting it.
This issue also appears on line 291 of the same file.
PasswordCredentialData credentialData = new PasswordCredentialData(cred.getHashIterations() != null ? cred.getHashIterations() : 0, cred.getAlgorithm());
Address review feedback on the null-handling fallbacks: Password: defaulting a missing hashIterations to 0 does not fix the problem, it relocates it. Both Pbkdf2PasswordHashProvider and Argon2PasswordHashProvider feed the stored iteration count back into the KDF at verify time, and a non-positive value is rejected there. Since the original hash parameters cannot be inferred, the credential could never be verified, so reject it with a ModelValidationException (HTTP 400) rather than persisting an unusable password. OTP: fall back to the target realm's OTP policy instead of the global default, so a realm configured for e.g. 8 digits or a 60-second period does not silently store incompatible 6-digit/30-second metadata. The counter now also uses the policy's initial counter, matching OTPCredentialModel.createFromPolicy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (3)
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:289
- The existing deprecated-credential test always supplies
hashIterations, so the reported null case and its new validation path have no regression coverage. Add an Admin REST test omitting this field that asserts the intended status/logging behavior; otherwise this fix can regress back to a 500 unnoticed.
if (cred.getHashIterations() == null) {
// The stored hash cannot be reproduced without the iteration count it was
// generated with, so such a credential could never be verified. Reject it
// instead of persisting an unusable password.
throw new ModelValidationException("Credential of type '" + cred.getType()
+ "' for user '" + user.getUsername() + "' is missing 'hashIterations'");
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:289
- This rejects the request with a 400 (and
UsersResourcelogs theModelValidationException) instead of applying the documented null-safe0fallback, so it does not implement the Fix/Test Plan or the issue's “no exception in logs” outcome. Either use the documented fallback here or update the PR's intended behavior and acceptance criteria to explicitly require rejection.
if (cred.getHashIterations() == null) {
// The stored hash cannot be reproduced without the iteration count it was
// generated with, so such a credential could never be verified. Reject it
// instead of persisting an unusable password.
throw new ModelValidationException("Credential of type '" + cred.getType()
+ "' for user '" + user.getUsername() + "' is missing 'hashIterations'");
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java:302
- These three new fallback branches are untested, although deprecated credential conversion already has integration coverage. Add TOTP/HOTP import cases with omitted
digits,counter, andperiodand verify the persisted credential data uses the realm policy values.
OTPPolicy otpPolicy = realm.getOTPPolicy();
OTPCredentialData credentialData = new OTPCredentialData(cred.getType(),
cred.getDigits() != null ? cred.getDigits() : otpPolicy.getDigits(),
cred.getCounter() != null ? cred.getCounter() : otpPolicy.getInitialCounter(),
cred.getPeriod() != null ? cred.getPeriod() : otpPolicy.getPeriod(),
cred.getAlgorithm(), null);
Summary
Closes #41640
Fixed a
NullPointerExceptioninRepresentationToModel.convertDeprecatedCredentialsFormat()that occurs when creating a user via the Admin REST API with credentials that have nullhashIterations.Root Cause
CredentialRepresentationdeclareshashIterations,digits,counter, andperiodas boxedInteger(nullable), butPasswordCredentialDataandOTPCredentialDataconstructors expect primitiveint. When any of these fields arenull, Java's auto-unboxing callsInteger.intValue()onnull, producing aNullPointerException.Fix
Added null-safe defaults (
0) for all nullableIntegerfields before passing them to the credential data constructors:cred.getHashIterations()→cred.getHashIterations() != null ? cred.getHashIterations() : 0cred.getDigits()→cred.getDigits() != null ? cred.getDigits() : 0cred.getCounter()→cred.getCounter() != null ? cred.getCounter() : 0cred.getPeriod()→cred.getPeriod() != null ? cred.getPeriod() : 0Changes
server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.javaTest plan