Skip to content

Added length check for username - #51129

Merged
rmartinc merged 1 commit into
keycloak:mainfrom
msdaly200:50903
Aug 10, 2026
Merged

Added length check for username#51129
rmartinc merged 1 commit into
keycloak:mainfrom
msdaly200:50903

Conversation

@msdaly200

Copy link
Copy Markdown
Contributor

Closes #50903

Added length check for username (255)

@rmartinc rmartinc 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.

Thanks @msdaly200! LGTM but please create a simple test (for example in LoginTests.java) that ensures the username detail is empty in the event. Something like:

    @Test
    public void loginMaxLengthUsername() {
        oauth.openLoginForm();
        loginPage.fillLogin("a".repeat(Validation.MAX_USERNAME_LENGTH + 1), "invalid");
        loginPage.submit();

        loginPage.assertCurrent();

        assertEquals("Invalid username or password.", loginPage.getUsernameInputError());

        EventAssertion.assertError(events.poll())
                .type(EventType.LOGIN_ERROR)
                .userId(null)
                .sessionId(null)
                .error(Errors.USER_NOT_FOUND)
                .withoutDetails(Details.USERNAME);
    }

@msdaly200
msdaly200 force-pushed the 50903 branch 3 times, most recently from 41be37e to 2747b61 Compare July 28, 2026 10:02
@msdaly200

Copy link
Copy Markdown
Contributor Author

Thanks @rmartinc I've added tests.

@msdaly200
msdaly200 marked this pull request as ready for review July 28, 2026 10:04
@msdaly200
msdaly200 requested a review from a team as a code owner July 28, 2026 10:04
Copilot AI balanced review requested due to automatic review settings July 28, 2026 10:04

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.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds a max-length constraint (255 chars) to username handling across multiple authentication flows to prevent overly long usernames from being processed, and introduces regression tests for key user-facing paths.

Changes:

  • Introduced Validation.MAX_USERNAME_LENGTH = 255 and applied it during username processing.
  • Added max-length validation in browser login (including organization login), reset password, and direct grant username validation.
  • Added/extended integration tests to cover max-length and whitespace-only username cases in UI flows.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/base/src/test/java/org/keycloak/tests/organization/authentication/OrganizationAuthenticationTest.java Adds a new test asserting org login rejects usernames longer than the max length.
tests/base/src/test/java/org/keycloak/tests/forms/ResetPasswordTest.java Adds a reset-password test for overlong usernames and asserts expected event/error behavior.
tests/base/src/test/java/org/keycloak/tests/forms/LoginTest.java Adds login tests for overlong and whitespace-only usernames (UI flow).
services/src/main/java/org/keycloak/services/validation/Validation.java Introduces a shared MAX_USERNAME_LENGTH constant (255).
services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java Trims username and rejects usernames exceeding max length in org login flow.
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java Trims username and rejects usernames exceeding max length in reset credential flow.
services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java Trims username and rejects usernames exceeding max length in direct grant flow.
services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java Rejects usernames exceeding max length in standard browser username form flow.

Copilot AI review requested due to automatic review settings July 28, 2026 16:11

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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

services/src/main/java/org/keycloak/services/validation/Validation.java:36

  • The inline comment is overly specific to a particular DDL/type (NVARCHAR(255)), which may be inaccurate across DB vendors and can become stale if schema definitions change. Consider making the comment DB-agnostic (e.g., referencing the username column length limit) or linking to the authoritative schema/migration source that defines the 255 limit.
    public static final int MAX_USERNAME_LENGTH = 255; // USER_ENTITY table NVARCHAR(255)

services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java:182

  • The same trim + max-length validation is now implemented in several authenticators (browser login, direct grant, reset password, organization flow). To reduce duplication and keep behavior consistent (including which error/response is returned), consider centralizing this into a small helper in Validation (or a shared authenticator utility) that performs trim + length check and returns a normalized username (or a standardized failure outcome). This will make future changes to username constraints less error-prone.
        // remove leading and trailing whitespace
        username = username.trim();

        if (username.length() > MAX_USERNAME_LENGTH) {
            context.getEvent().error(Errors.USER_NOT_FOUND);
            Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), FIELD_USERNAME);
            context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
            return null;
        }

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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java:152

  • A successful passkey submission sets context.getUser() but does not include a username field (the passkey form only posts WebAuthn fields), so this unconditional trim throws a NullPointerException and breaks passkey login when the organization authenticator is enabled. Preserve the existing user-resolved path by only normalizing and validating a non-null submitted username.
        // remove leading and trailing whitespace
        username = username.trim();

        if (username.length() > MAX_USERNAME_LENGTH) {

Copilot AI review requested due to automatic review settings July 29, 2026 15:01

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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 30, 2026 09:08

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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java:116

  • dummyHash performs a full password hash, but this reset-credential step never hashes a password for either existing or missing users. Calling it only for over-length input adds an expensive unauthenticated CPU path without providing timing protection; reject the value directly as the surrounding reset flow does.
            AuthenticatorUtils.dummyHash(context);

services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java:151

  • This username-only organization step does not hash passwords for normal valid or unknown usernames, so dummyHash makes only the over-length case perform a full password hash. That exposes an unnecessary unauthenticated CPU-expensive path; reject the input without hashing here.
            AuthenticatorUtils.dummyHash(context);

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:627

  • This validator does not provide a non-overridable 255-character cap for realms with a custom user profile. The profile decorator removes built-in validators whose IDs occur in the system profile (including length) and then applies the realm's validator, so omitting length falls back to 2048 and configuring a larger maximum replaces this guard; use a dedicated non-configurable username-cap validator or explicitly cap the configured maximum.
        return new AttributeValidatorMetadata(LengthValidator.ID,
                ValidatorConfig.builder()
                        .config(LengthValidator.KEY_MIN, "1")
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))
                        .build());

tests/base/src/test/java/org/keycloak/tests/oauth/DirectGrantInputValidationTest.java:47

  • This boundary test cannot prove that a 255-character username passed the new guard: both the normal user-not-found path and an erroneous >= MAX_USERNAME_LENGTH rejection return the same 400 invalid_grant. Exercise an existing boundary-length user (expect success) or assert the emitted username detail so the test fails if the guard rejects the boundary value.
        AccessTokenResponse response = doGrant("a".repeat(Validation.MAX_USERNAME_LENGTH), "password");
        assertEquals(400, response.getStatusCode());
        assertEquals("invalid_grant", response.getError());

Copilot AI review requested due to automatic review settings August 7, 2026 10:51

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:627

  • LengthValidator trims by default, while the user-profile persistence path preserves nonblank leading/trailing whitespace. A username containing 255 characters plus a long whitespace suffix therefore passes this check but is still written over the USER_ENTITY limit; disable trimming for this storage-length constraint.
                ValidatorConfig.builder()
                        .config(LengthValidator.KEY_MIN, "1")
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))
                        .build());

tests/base/src/test/java/org/keycloak/tests/oauth/DirectGrantInputValidationTest.java:47

  • This boundary test cannot detect an off-by-one regression because both the over-limit branch and the normal unknown-user path return the same 400/invalid_grant; changing the guard to >= would leave both tests green. Make the 255-character case observably reach user lookup, for example by granting successfully as an injected boundary-length user or asserting that the error event contains Details.USERNAME.
        AccessTokenResponse response = doGrant("a".repeat(Validation.MAX_USERNAME_LENGTH), "password");
        assertEquals(400, response.getStatusCode());
        assertEquals("invalid_grant", response.getError());

Copilot AI review requested due to automatic review settings August 7, 2026 11:04

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:628

  • LengthValidator trims by default, but user-profile persistence does not trim usernames (DefaultAttributes.java:472-494). A raw username longer than 255 characters can therefore pass when the excess is surrounding whitespace and then reach the VARCHAR(255) column; disable validator trimming so the bound applies to the stored value.
        return new AttributeValidatorMetadata(LengthValidator.ID,
                ValidatorConfig.builder()
                        .config(LengthValidator.KEY_MIN, "1")
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))
                        .build());

@mabartos mabartos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@msdaly200 Thanks for the PR. I've just added a few comments, mainly for the dummyHash as Copilot suggested. Let me know what you think. Thanks!

@mabartos mabartos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, just one thing related to tests

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

services/src/main/java/org/keycloak/organization/authentication/authenticators/browser/OrganizationAuthenticator.java:153

  • This guard only validates the submitted form value. authenticate() still passes the unbounded login_hint client note directly to action(context, loginHint), so an unauthenticated oversized username can bypass this check and reach organization/user lookup and flow state. Normalize and validate the hint through the same path before calling the private action.
        if (AuthenticatorUtils.isUsernameTooLong(username)) {
            context.getEvent().error(Errors.USER_NOT_FOUND);
            Response challengeResponse = context.form()
                .addError(new FormMessage(UserModel.USERNAME, Messages.INVALID_USERNAME))
                .createLoginUsername();
            context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);

tests/base/src/test/java/org/keycloak/tests/broker/IdpCreateUserIfUniqueAuthenticatorTest.java:99

  • This test does not exercise the new IdpCreateUserIfUniqueAuthenticator guard: the default first-broker-login flow runs idp-review-profile first, and the newly added IDP_REVIEW length validator already renders this same page for the oversized value. Configure review-profile not to validate/show before asserting the later resetFlow(), otherwise removing the guard under test would still leave this test green.
        assertTrue("login-idp-review-user-profile".equals(driver.page().getCurrentPageId()),
                "Expected broker flow to restart at IDP review step after resetFlow(), but page was: "
                        + driver.page().getCurrentPageId());

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:628

  • LengthValidator trims by default, but user-profile persistence keeps the original username. Consequently, a raw value such as 255 characters plus a trailing space passes this 255-character check and can still exceed the USER_ENTITY column; disable trimming for this storage-bound validation.
        return new AttributeValidatorMetadata(LengthValidator.ID,
                ValidatorConfig.builder()
                        .config(LengthValidator.KEY_MIN, "1")
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))
                        .build());

Copilot AI review requested due to automatic review settings August 7, 2026 15:12

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

services/src/main/java/org/keycloak/authentication/authenticators/util/AuthenticatorUtils.java:91

  • This blanket dummyHash requirement is incorrect: reset-credential and broker callers do not verify passwords and intentionally reject without hashing to avoid unauthenticated CPU amplification. Document only the required failure response rather than instructing every caller to hash.
     * Returns {@code true} if {@code username} exceeds the maximum length allowed by the
     * USER_ENTITY table column (VARCHAR(255)). Callers should invoke {@link #dummyHash(AuthenticationFlowContext)}
     * and return their appropriate failure response when this returns {@code true}.

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:628

  • LengthValidator trims by default, so a stored username such as 255 characters plus trailing whitespace passes this max check even though its actual value exceeds the 255-character column limit; in the broker flow it can also loop between review and the raw-length guard. Disable trimming for this storage-bound validation so the full persisted value is counted.
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))

tests/base/src/test/java/org/keycloak/tests/oauth/DirectGrantInputValidationTest.java:47

  • This boundary test cannot detect an off-by-one regression: both the new oversized guard and the normal unknown-user path return the same 400 invalid_grant, so it still passes if length 255 is rejected. Authenticate a real 255-character username or assert the emitted username detail to distinguish the paths.
        AccessTokenResponse response = doGrant("a".repeat(Validation.MAX_USERNAME_LENGTH), "password");
        assertEquals(400, response.getStatusCode());
        assertEquals("invalid_grant", response.getError());

Signed-off-by: Marie Daly <marie.daly1@ibm.com>
Copilot AI review requested due to automatic review settings August 10, 2026 07:53

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (3)

services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java:628

  • LengthValidator trims before measuring, while user-profile normalization preserves the original username. A value such as 255 characters plus a very large trailing-space suffix therefore passes this validator and can still be sent to the VARCHAR(255) column; disable trimming for this storage bound.
                ValidatorConfig.builder()
                        .config(LengthValidator.KEY_MIN, "1")
                        .config(LengthValidator.KEY_MAX, String.valueOf(MAX_USERNAME_LENGTH))

services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java:81

  • This guard is bypassed by IdpDetectExistingBrokerUserAuthenticator, which overrides authenticateImpl without calling this implementation. Its oversized username still reaches checkExistingUser and is logged/rendered verbatim at IdpDetectExistingBrokerUserAuthenticator.java:58-64, retaining the amplification path for realms using that first-login flow; move the guard into shared preprocessing or add it to the override.
        if (AuthenticatorUtils.isUsernameTooLong(username)) {
            ServicesLogger.LOGGER.resetFlow("Username exceeds maximum length");
            context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
            context.resetFlow();
            return;

services/src/main/java/org/keycloak/authentication/authenticators/util/AuthenticatorUtils.java:91

  • The new call sites all reject oversized input without dummyHash, and the reset-credential path intentionally must not add a password hash to an unauthenticated request. Requiring every future caller to hash is therefore incorrect and risks CPU amplification; remove that mandate from the method contract.
     * Returns {@code true} if {@code username} exceeds the maximum length allowed by the
     * USER_ENTITY table column (VARCHAR(255)). Callers should invoke {@link #dummyHash(AuthenticationFlowContext)}
     * and return their appropriate failure response when this returns {@code true}.

@mabartos mabartos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@msdaly200 LGTM, thanks!

@rmartinc rmartinc 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.

Thanks @msdaly200 for the PR and @mabartos for the review!

@rmartinc
rmartinc merged commit 0e728be into keycloak:main Aug 10, 2026
92 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

flaky-test status/hold PR should not be merged. On hold for later. team/core-authn

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing input length validation on username field causes outsized log generation in /login-actions/authenticate

4 participants