Skip to content

Conversation

@akshayutture-augment
Copy link

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@akshayutture-augment
Copy link
Author

@greptileai

@greptile-apps
Copy link

greptile-apps bot commented Nov 14, 2025

Greptile Overview

Confidence Score: 0/5

  • This PR has critical security vulnerabilities and should not be merged
  • Score of 0 reflects critical security flaw where recovery codes are stored unhashed in user storage, creating a significant security risk. If a user storage backend is compromised, all recovery codes would be exposed in plain text. Additionally, there's an unhandled error condition that could cause runtime exceptions.
  • Pay critical attention to CredentialHelper.java and BackwardsCompatibilityUserStorage.java - both contain security vulnerabilities that must be fixed before merge

Important Files Changed

File Analysis

Filename Score Overview
server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java 1/5 Added createRecoveryCodesCredential helper with critical security flaw - stores unhashed codes in user storage
services/src/main/java/org/keycloak/authentication/requiredactions/RecoveryAuthnCodesAction.java 3/5 Refactored to use CredentialHelper.createRecoveryCodesCredential - clean refactor but inherits security issue
services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java 4/5 Updated to use RecoveryAuthnCodesUtils.getCredential() - potential NPE if credential not found
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java 1/5 Implements recovery codes support with plain text validation instead of hash verification - critical flaw

Sequence Diagram

sequenceDiagram
    participant User
    participant RecoveryAuthnCodesAction
    participant CredentialHelper
    participant UserStorage
    participant LocalDB
    participant Authenticator
    
    Note over User,LocalDB: Recovery Codes Setup Flow
    User->>RecoveryAuthnCodesAction: Setup recovery codes
    RecoveryAuthnCodesAction->>RecoveryAuthnCodesAction: Generate raw codes
    RecoveryAuthnCodesAction->>RecoveryAuthnCodesAction: Create credential model
    RecoveryAuthnCodesAction->>CredentialHelper: createRecoveryCodesCredential(model, rawCodes)
    CredentialHelper->>CredentialHelper: Serialize raw codes to JSON
    CredentialHelper->>UserStorage: updateCredential(rawCodesJson)
    alt User Storage accepts credential
        UserStorage-->>CredentialHelper: true (stored in UserStorage)
        Note over UserStorage: SECURITY ISSUE: Stores unhashed codes
    else User Storage rejects credential
        UserStorage-->>CredentialHelper: false
        CredentialHelper->>LocalDB: createCredential(hashedModel)
        Note over LocalDB: Stores hashed codes correctly
        LocalDB-->>CredentialHelper: Credential created
    end
    CredentialHelper-->>RecoveryAuthnCodesAction: Success
    RecoveryAuthnCodesAction-->>User: Display codes to save
    
    Note over User,LocalDB: Recovery Codes Authentication Flow
    User->>Authenticator: Enter recovery code
    Authenticator->>Authenticator: RecoveryAuthnCodesUtils.getCredential()
    Authenticator->>UserStorage: getFederatedCredentialsStream()
    alt Credential in UserStorage
        UserStorage-->>Authenticator: Recovery codes credential
        Authenticator->>UserStorage: isValid(inputCode)
        UserStorage->>UserStorage: Plain text comparison
        Note over UserStorage: SECURITY ISSUE: Compares unhashed
        UserStorage-->>Authenticator: Valid/Invalid
    else Credential in LocalDB
        UserStorage-->>Authenticator: Not found
        Authenticator->>LocalDB: getStoredCredentialsByType()
        LocalDB-->>Authenticator: Recovery codes credential
        Authenticator->>LocalDB: isValid(inputCode)
        LocalDB->>LocalDB: Hash input and compare
        Note over LocalDB: Secure: Uses hash verification
        LocalDB-->>Authenticator: Valid/Invalid
    end
    Authenticator-->>User: Authentication result
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +119 to +123
recoveryCodesJson = JsonSerialization.writeValueAsString(generatedCodes);
} catch (IOException e) {
throw new RuntimeException(e);
}
UserCredentialModel recoveryCodesCredential = new UserCredentialModel("", credentialModel.getType(), recoveryCodesJson);
Copy link

Choose a reason for hiding this comment

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

logic: Storing unhashed recovery codes in user storage is a critical security vulnerability. The generatedCodes list contains plain text codes, but user storage providers may persist these to their backend systems. When stored in local DB via createCredential, codes are hashed (via RecoveryAuthnCodesCredentialModel.createFromValues), but when stored in user storage via updateCredential, they remain unhashed.

Suggested change
recoveryCodesJson = JsonSerialization.writeValueAsString(generatedCodes);
} catch (IOException e) {
throw new RuntimeException(e);
}
UserCredentialModel recoveryCodesCredential = new UserCredentialModel("", credentialModel.getType(), recoveryCodesJson);
String recoveryCodesJson;
try {
// Hash the codes before storing, consistent with local storage
List<String> hashedCodes = generatedCodes.stream()
.map(RecoveryAuthnCodesUtils::hashRawCode)
.collect(Collectors.toList());
recoveryCodesJson = JsonSerialization.writeValueAsString(hashedCodes);
Prompt To Fix With AI
This is a comment left during a code review.
Path: server-spi-private/src/main/java/org/keycloak/utils/CredentialHelper.java
Line: 119:123

Comment:
**logic:** Storing unhashed recovery codes in user storage is a critical security vulnerability. The `generatedCodes` list contains plain text codes, but user storage providers may persist these to their backend systems. When stored in local DB via `createCredential`, codes are hashed (via `RecoveryAuthnCodesCredentialModel.createFromValues`), but when stored in user storage via `updateCredential`, they remain unhashed.

```suggestion
        String recoveryCodesJson;
        try {
            // Hash the codes before storing, consistent with local storage
            List<String> hashedCodes = generatedCodes.stream()
                .map(RecoveryAuthnCodesUtils::hashRawCode)
                .collect(Collectors.toList());
            recoveryCodesJson = JsonSerialization.writeValueAsString(hashedCodes);
```

How can I resolve this? If you propose a fix, please make it concise.

return false;
}

return generatedKeys.stream().anyMatch(key -> key.equals(input.getChallengeResponse()));
Copy link

Choose a reason for hiding this comment

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

logic: Plain text comparison used instead of hash verification. Should use RecoveryAuthnCodesUtils.verifyRecoveryCodeInput() to validate hashed codes consistently with the main credential provider.

Suggested change
return generatedKeys.stream().anyMatch(key -> key.equals(input.getChallengeResponse()));
return generatedKeys.stream().anyMatch(key -> RecoveryAuthnCodesUtils.verifyRecoveryCodeInput(input.getChallengeResponse(), key));
Prompt To Fix With AI
This is a comment left during a code review.
Path: testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/BackwardsCompatibilityUserStorage.java
Line: 340:340

Comment:
**logic:** Plain text comparison used instead of hash verification. Should use `RecoveryAuthnCodesUtils.verifyRecoveryCodeInput()` to validate hashed codes consistently with the main credential provider.

```suggestion
            return generatedKeys.stream().anyMatch(key -> RecoveryAuthnCodesUtils.verifyRecoveryCodeInput(input.getChallengeResponse(), key));
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 13 to 15

private final int codeNumber;

Copy link

Choose a reason for hiding this comment

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

logic: Calling .get() on Optional without checking isPresent() first will throw NoSuchElementException if credential not found

Suggested change
private final int codeNumber;
Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
if (!credentialModelOpt.isPresent()) {
throw new RuntimeException("Recovery codes credential not found for user");
}
RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModelOpt.get());
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/src/main/java/org/keycloak/forms/login/freemarker/model/RecoveryAuthnCodeInputLoginBean.java
Line: 13:15

Comment:
**logic:** Calling `.get()` on `Optional` without checking `isPresent()` first will throw `NoSuchElementException` if credential not found

```suggestion
        Optional<CredentialModel> credentialModelOpt = RecoveryAuthnCodesUtils.getCredential(user);
        if (!credentialModelOpt.isPresent()) {
            throw new RuntimeException("Recovery codes credential not found for user");
        }
        RecoveryAuthnCodesCredentialModel recoveryCodeCredentialModel = RecoveryAuthnCodesCredentialModel.createFromCredentialModel(credentialModelOpt.get());
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants