VYPR
Low severity3.7NVD Advisory· Published Mar 23, 2026· Updated Apr 1, 2026

CVE-2026-4633

CVE-2026-4633

Description

A flaw was found in Keycloak. A remote attacker can exploit differential error messages during the identity-first login flow when Organizations are enabled. This vulnerability allows an attacker to determine the existence of users, leading to information disclosure through user enumeration.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.keycloak:keycloak-servicesMaven
>= 26.5.0, < 26.6.026.6.0
org.keycloak:keycloak-servicesMaven
<= 26.4.7

Affected products

1

Patches

2
b4558a874fa7

Keycloak user enumeration via identity-first login

https://github.com/keycloak/keycloakVlasta RamikApr 8, 2026via ghsa
8 files changed · +122 10
  • js/apps/account-ui/test/groups.spec.ts+3 0 modified
    @@ -2,10 +2,12 @@ import { expect, test } from "@playwright/test";
     import groupsRealm from "./realms/groups-realm.json" with { type: "json" };
     import { login } from "./support/actions.ts";
     import { createTestBed } from "./support/testbed.ts";
    +import { waitForRealmReady } from "./support/test-utils.ts";
     
     test.describe("Groups", () => {
       test("lists groups", async ({ page }) => {
         await using testBed = await createTestBed(groupsRealm);
    +    await waitForRealmReady();
     
         await login(page, testBed.realm);
         await page.getByTestId("groups").click();
    @@ -14,6 +16,7 @@ test.describe("Groups", () => {
     
       test("lists direct and indirect groups", async ({ page }) => {
         await using testBed = await createTestBed(groupsRealm);
    +    await waitForRealmReady();
     
         await login(page, testBed.realm, "alice", "alice");
         await page.getByTestId("groups").click();
    
  • services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java+1 5 modified
    @@ -261,11 +261,7 @@ protected boolean isDisabledByBruteForce(AuthenticationFlowContext context, User
         }
     
         protected String getDefaultChallengeMessage(AuthenticationFlowContext context) {
    -        if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) {
    -            return Messages.INVALID_PASSWORD;
    -        } else {
    -            return Messages.INVALID_USER;
    -        }
    +        return Messages.INVALID_USER;
         }
     
         protected boolean isUserAlreadySetBeforeUsernamePasswordAuth(AuthenticationFlowContext context) {
    
  • test-framework/ui/src/main/java/org/keycloak/testframework/ui/page/PasswordPage.java+8 0 modified
    @@ -48,6 +48,14 @@ public String getPassword() {
             return passwordInput.getAttribute("value");
         }
     
    +    public String getPasswordError() {
    +        try {
    +            return passwordError.getText();
    +        } catch (NoSuchElementException e) {
    +            return null;
    +        }
    +    }
    +
         public String getError() {
             try {
                 return loginErrorMessage.getText();
    
  • tests/base/src/test/java/org/keycloak/tests/login/LoginErrorMessageTest.java+105 0 added
    @@ -0,0 +1,105 @@
    +/*
    + * Copyright 2025 Red Hat, Inc. and/or its affiliates
    + * and other contributors as indicated by the @author tags.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + * http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +package org.keycloak.tests.login;
    +
    +import org.keycloak.models.AuthenticationExecutionModel;
    +import org.keycloak.testframework.annotations.InjectRealm;
    +import org.keycloak.testframework.annotations.InjectUser;
    +import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
    +import org.keycloak.testframework.injection.LifeCycle;
    +import org.keycloak.testframework.oauth.OAuthClient;
    +import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
    +import org.keycloak.testframework.realm.ManagedRealm;
    +import org.keycloak.testframework.realm.ManagedUser;
    +import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
    +import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
    +import org.keycloak.testframework.ui.annotations.InjectPage;
    +import org.keycloak.testframework.ui.page.LoginUsernamePage;
    +import org.keycloak.testframework.ui.page.PasswordPage;
    +import org.keycloak.tests.common.BasicUserConfig;
    +import org.keycloak.testsuite.util.FlowUtil;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import static org.junit.jupiter.api.Assertions.assertEquals;
    +
    +/**
    + * Verifies that login error messages do not leak whether a username is valid,
    + * preventing user enumeration attacks.
    + *
    + * In an identity-first flow (UsernameForm → UsernamePasswordForm), after the
    + * username step resolves the user, UsernamePasswordForm runs with
    + * USER_SET_BEFORE_USERNAME_PASSWORD_AUTH=true. A wrong password must show
    + * a generic "Invalid username or password." error, not "Invalid password."
    + * which would confirm the user exists.
    + */
    +@KeycloakIntegrationTest
    +public class LoginErrorMessageTest {
    +
    +    private static final String IDENTITY_FIRST_FLOW = "identity-first-browser";
    +
    +    @InjectRealm(lifecycle = LifeCycle.METHOD)
    +    ManagedRealm realm;
    +
    +    @InjectUser(config = BasicUserConfig.class)
    +    ManagedUser user;
    +
    +    @InjectOAuthClient
    +    OAuthClient oauth;
    +
    +    @InjectPage
    +    LoginUsernamePage usernamePage;
    +
    +    @InjectPage
    +    PasswordPage passwordPage;
    +
    +    @InjectRunOnServer
    +    RunOnServerClient runOnServer;
    +
    +    @Test
    +    public void testWrongPasswordShowsGenericErrorWhenUserPreEstablished() {
    +        configureIdentityFirstFlow();
    +
    +        // Step 1: enter valid username on the username-only page
    +        oauth.openLoginForm();
    +        usernamePage.assertCurrent();
    +        usernamePage.fillLoginWithUsernameOnly("basic-user");
    +        usernamePage.submit();
    +
    +        // Step 2: UsernamePasswordForm renders with username hidden (user was pre-set).
    +        // Use PasswordPage to interact with the password-only form.
    +        passwordPage.fillPassword("wrong-password");
    +        passwordPage.submit();
    +
    +        // The error must be generic "Invalid username or password." — not
    +        // "Invalid password." which would confirm the username is valid.
    +        assertEquals("Invalid username or password.", passwordPage.getPasswordError());
    +    }
    +
    +    private void configureIdentityFirstFlow() {
    +        runOnServer.run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(IDENTITY_FIRST_FLOW));
    +        runOnServer.run(session -> FlowUtil.inCurrentRealm(session)
    +                .selectFlow(IDENTITY_FIRST_FLOW)
    +                .inForms(forms -> forms
    +                        .clear()
    +                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "auth-username-form")
    +                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "auth-username-password-form")
    +                )
    +                .defineAsBrowserFlow()
    +        );
    +    }
    +}
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java+1 1 modified
    @@ -149,7 +149,7 @@ public void usernamePasswordFormReauthentication() {
             // Try bad password and assert things still hidden
             loginPage.login("bad-password");
             loginPage.assertCurrent();
    -        Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +        Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
             assertUsernameFieldAndOtherFields(false);
             assertInfoMessageAboutReAuthenticate(false);
     
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/authentication/OrganizationAuthenticationTest.java+1 1 modified
    @@ -273,7 +273,7 @@ public void testDuplicateEmailsEnabled() {
             loginPage.loginUsername(duplicatedUser.getEmail());
             loginPage.clickSignIn();
             loginPage.login(duplicatedUser.getEmail());
    -        assertThat(loginPage.getInputError(), is("Invalid password."));
    +        assertThat(loginPage.getInputError(), is("Invalid username or password."));
     
             // trying to authenticate to the account that has the email as username is ok
             oauth.loginForm().open();
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysOrganizationAuthenticationTest.java+1 1 modified
    @@ -215,7 +215,7 @@ public void passwordLoginWithNonDiscoverableKey() throws Exception {
                 MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());
                 loginPage.login("invalid-password");
                 loginPage.assertCurrent();
    -            MatcherAssert.assertThat(loginPage.getPasswordInputError(), Matchers.is("Invalid password."));
    +            MatcherAssert.assertThat(loginPage.getPasswordInputError(), Matchers.is("Invalid username or password."));
                 events.expect(EventType.LOGIN_ERROR)
                         .error(Errors.INVALID_USER_CREDENTIALS)
                         .user(user.getId())
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java+2 2 modified
    @@ -363,7 +363,7 @@ public void webauthnLoginWithExternalKey_reauthenticationWithPasswordOrPasskey()
     
                 // incorrect password (password of different user)
                 loginPage.login(getPassword("test-user@localhost"));
    -            Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +            Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
     
                 // Check that passkeys elements still available for this user
                 MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());
    @@ -390,7 +390,7 @@ public void webauthnLoginWithExternalKey_reauthenticationWithPasswordOrPasskey()
     
                 // incorrect password (password of different user)
                 loginPage.login(getPassword("test-user@localhost"));
    -            Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +            Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
     
                 events.clear();
     
    
b137016cc6dc

Keycloak user enumeration via identity-first login

https://github.com/keycloak/keycloakVlasta RamikApr 8, 2026via ghsa
6 files changed · +100 10
  • services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java+1 5 modified
    @@ -261,11 +261,7 @@ protected boolean isDisabledByBruteForce(AuthenticationFlowContext context, User
         }
     
         protected String getDefaultChallengeMessage(AuthenticationFlowContext context) {
    -        if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) {
    -            return Messages.INVALID_PASSWORD;
    -        } else {
    -            return Messages.INVALID_USER;
    -        }
    +        return Messages.INVALID_USER;
         }
     
         protected boolean isUserAlreadySetBeforeUsernamePasswordAuth(AuthenticationFlowContext context) {
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java+1 1 modified
    @@ -147,7 +147,7 @@ public void usernamePasswordFormReauthentication() {
             // Try bad password and assert things still hidden
             loginPage.login("bad-password");
             loginPage.assertCurrent();
    -        Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +        Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
             assertUsernameFieldAndOtherFields(false);
             assertInfoMessageAboutReAuthenticate(false);
     
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/login/LoginErrorMessageTest.java+94 0 added
    @@ -0,0 +1,94 @@
    +/*
    + * Copyright 2025 Red Hat, Inc. and/or its affiliates
    + * and other contributors as indicated by the @author tags.
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + * http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +package org.keycloak.testsuite.login;
    +
    +import org.jboss.arquillian.graphene.page.Page;
    +import org.junit.Test;
    +import org.keycloak.models.AuthenticationExecutionModel;
    +import org.keycloak.representations.idm.RealmRepresentation;
    +import org.keycloak.representations.idm.UserRepresentation;
    +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
    +import org.keycloak.testsuite.pages.LoginUsernameOnlyPage;
    +import org.keycloak.testsuite.pages.PasswordPage;
    +import org.keycloak.testsuite.util.FlowUtil;
    +import org.keycloak.testsuite.util.UserBuilder;
    +
    +import static org.junit.Assert.assertEquals;
    +
    +/**
    + * Verifies that login error messages do not leak whether a username is valid,
    + * preventing user enumeration attacks.
    + *
    + * In an identity-first flow (UsernameForm → UsernamePasswordForm), after the
    + * username step resolves the user, UsernamePasswordForm runs with
    + * USER_SET_BEFORE_USERNAME_PASSWORD_AUTH=true. A wrong password must show
    + * a generic "Invalid username or password." error, not "Invalid password."
    + * which would confirm the user exists.
    + */
    +public class LoginErrorMessageTest extends AbstractTestRealmKeycloakTest {
    +
    +    private static final String IDENTITY_FIRST_FLOW = "identity-first-browser";
    +
    +    @Page
    +    protected LoginUsernameOnlyPage loginUsernameOnlyPage;
    +
    +    @Page
    +    protected PasswordPage passwordPage;
    +
    +    @Override
    +    public void configureTestRealm(RealmRepresentation testRealm) {
    +        UserRepresentation user = UserBuilder.create()
    +                .username("basic-user")
    +                .password("password")
    +                .email("basic@localhost")
    +                .firstName("First")
    +                .lastName("Last")
    +                .enabled(true)
    +                .build();
    +        testRealm.getUsers().add(user);
    +    }
    +
    +    @Test
    +    public void testWrongPasswordShowsGenericErrorWhenUserPreEstablished() {
    +        configureIdentityFirstFlow();
    +
    +        // Step 1: enter valid username on the username-only page
    +        oauth.openLoginForm();
    +        loginUsernameOnlyPage.login("basic-user");
    +
    +        // Step 2: UsernamePasswordForm renders with username hidden (user was pre-set).
    +        // Use PasswordPage to interact with the password-only form.
    +        passwordPage.login("wrong-password");
    +
    +        // The error must be generic "Invalid username or password." — not
    +        // "Invalid password." which would confirm the username is valid.
    +        assertEquals("Invalid username or password.", passwordPage.getPasswordError());
    +    }
    +
    +    private void configureIdentityFirstFlow() {
    +        testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(IDENTITY_FIRST_FLOW));
    +        testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session)
    +                .selectFlow(IDENTITY_FIRST_FLOW)
    +                .inForms(forms -> forms
    +                        .clear()
    +                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "auth-username-form")
    +                        .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, "auth-username-password-form")
    +                )
    +                .defineAsBrowserFlow()
    +        );
    +    }
    +}
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/organization/authentication/OrganizationAuthenticationTest.java+1 1 modified
    @@ -270,7 +270,7 @@ public void testDuplicateEmailsEnabled() {
             loginPage.loginUsername(duplicatedUser.getEmail());
             loginPage.clickSignIn();
             loginPage.login(duplicatedUser.getEmail());
    -        assertThat(loginPage.getInputError(), is("Invalid password."));
    +        assertThat(loginPage.getInputError(), is("Invalid username or password."));
     
             // trying to authenticate to the account that has the email as username is ok
             oauth.loginForm().open();
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysOrganizationAuthenticationTest.java+1 1 modified
    @@ -213,7 +213,7 @@ public void passwordLoginWithNonDiscoverableKey() throws Exception {
                 MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());
                 loginPage.login("invalid-password");
                 loginPage.assertCurrent();
    -            MatcherAssert.assertThat(loginPage.getPasswordInputError(), Matchers.is("Invalid password."));
    +            MatcherAssert.assertThat(loginPage.getPasswordInputError(), Matchers.is("Invalid username or password."));
                 events.expect(EventType.LOGIN_ERROR)
                         .error(Errors.INVALID_USER_CREDENTIALS)
                         .user(user.getId())
    
  • testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/webauthn/passwordless/PasskeysUsernamePasswordFormTest.java+2 2 modified
    @@ -361,7 +361,7 @@ public void webauthnLoginWithExternalKey_reauthenticationWithPasswordOrPasskey()
     
                 // incorrect password (password of different user)
                 loginPage.login(getPassword("test-user@localhost"));
    -            Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +            Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
     
                 // Check that passkeys elements still available for this user
                 MatcherAssert.assertThat(driver.findElement(By.xpath("//form[@id='webauth']")), Matchers.notNullValue());
    @@ -388,7 +388,7 @@ public void webauthnLoginWithExternalKey_reauthenticationWithPasswordOrPasskey()
     
                 // incorrect password (password of different user)
                 loginPage.login(getPassword("test-user@localhost"));
    -            Assert.assertEquals("Invalid password.", loginPage.getInputError());
    +            Assert.assertEquals("Invalid username or password.", loginPage.getInputError());
     
                 events.clear();
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.