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.
| Package | Affected versions | Patched versions |
|---|---|---|
org.keycloak:keycloak-servicesMaven | >= 26.5.0, < 26.6.0 | 26.6.0 |
org.keycloak:keycloak-servicesMaven | <= 26.4.7 | — |
Affected products
1- cpe:2.3:a:redhat:build_of_keycloak:-:*:*:*:-:*:*:*
Patches
2b4558a874fa7Keycloak user enumeration via identity-first login
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();
b137016cc6dcKeycloak user enumeration via identity-first login
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- bugzilla.redhat.com/show_bug.cginvdExploitIssue TrackingVendor AdvisoryWEB
- access.redhat.com/security/cve/CVE-2026-4633nvdVendor AdvisoryWEB
- github.com/advisories/GHSA-rhgq-f8x5-j2jcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-4633ghsaADVISORY
- github.com/keycloak/keycloak/commit/b137016cc6dcfd9f59b2aa2e6d73af8b0ebf7c6eghsaWEB
- github.com/keycloak/keycloak/commit/b4558a874fa79341404ae4d2d8f240f22bfed340ghsaWEB
- github.com/keycloak/keycloak/issues/47619ghsaWEB
News mentions
0No linked articles in our index yet.