Skip to content

Fail fast when UserDetailsService returns null in onLoginSuccess - #19552

Open
skdas20 wants to merge 1 commit into
spring-projects:mainfrom
skdas20:gh-19535-remember-me-null-userdetails
Open

Fail fast when UserDetailsService returns null in onLoginSuccess#19552
skdas20 wants to merge 1 commit into
spring-projects:mainfrom
skdas20:gh-19535-remember-me-null-userdetails

Conversation

@skdas20

@skdas20 skdas20 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

When the successful Authentication carries no credentials, onLoginSuccess falls back to looking the password up through the configured UserDetailsService, but dereferences the result without checking it:

UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();

A UserDetailsService that returns null rather than throwing UsernameNotFoundException therefore shows up as a bare NullPointerException from inside the remember-me filter, with nothing in the stack trace pointing at the service that is actually misconfigured.

processAutoLoginCookie in this same class already guards the identical call, and is explicit about what a null return means:

UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
Assert.notNull(userDetails, () -> "UserDetailsService " + getUserDetailsService()
        + " returned null for username " + cookieTokens[0] + ". "
        + "This is an interface contract violation");

autoLoginClearsCookieIfUserServiceMisconfigured pins that behaviour in the tests. This change applies the same guard to onLoginSuccess so both lookups report the misconfiguration the same way, and adds loginSuccessFailsIfUserServiceMisconfigured alongside the existing loginSuccess tests. Without the production change that test fails with the reported NullPointerException.

One note on the approach: gh-19535 suggests skipping cookie generation instead, with

password = (user == null) ? password : user.getPassword();

I went with the existing in-class precedent rather than that, because silently continuing would quietly swallow a misconfiguration that the class elsewhere treats as a contract violation — and the user would get no remember-me cookie with no indication why. Happy to switch to the lenient form if you'd rather keep onLoginSuccess tolerant; it's a small change either way.

Closes gh-19535

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 14, 2026
@skylarsutton

skylarsutton commented Aug 19, 2026

Copy link
Copy Markdown

I guess my objection to this strategy is that, while it does conform to the guards found in processAutoLoginCookie it does not conform to the behavior within the rest of the method (onLoginSuccess).

Two lines above your proposed change, if the username cannot be obtained it does not fail with an assertion error, it simply returns without generating a cookie:

if (!StringUtils.hasLength(username)) {
    this.logger.debug("Unable to retrieve username");
    return;
}

Two lines below your proposed change, if the user can be obtained but has no password, it does not fail with an assertion error, it ALSO simply returns without generating a cookie:

password = user.getPassword();
if (!StringUtils.hasLength(password)) {
    this.logger.debug("Unable to obtain password for user: " + username);
    return;
}

IMHO - the implication is that onLoginSuccess intentionally allows nulls, and nulls mean "do not generate a cookie".

onLoginSuccess dereferenced the result of loadUserByUsername without a
null check, so a UserDetailsService returning null instead of throwing
UsernameNotFoundException surfaced as a bare NullPointerException from
inside the remember-me filter.

Treat a null user the same way the surrounding code treats a missing
username and a missing password: log at debug and return without
generating a cookie.

Closes spring-projectsgh-19535

Signed-off-by: skdas20 <skdas5405@gmail.com>
@skdas20

skdas20 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@skylarsutton that's a fair point and I've switched to it — thanks for pushing back.

You're right that the local pattern is the more relevant one. I anchored on processAutoLoginCookie because it guards the same call, but that's a different method with a different job: it's validating an untrusted cookie, where a null user genuinely is a broken configuration worth surfacing. onLoginSuccess is on the happy path after a successful authentication, and both of its neighbouring guards treat "couldn't get what I need" as a reason to skip the cookie rather than fail. Throwing there would also mean a misconfigured UserDetailsService breaks login itself, not just remember-me, which is a worse outcome than silently not issuing a cookie.

Now reads:

UserDetails user = getUserDetailsService().loadUserByUsername(username);
if (user == null) {
    this.logger.debug("Unable to obtain user details for user: " + username);
    return;
}
password = user.getPassword();

The test asserts no remember-me cookie is written rather than expecting an exception, and the commit message has been updated to match. Full class is green (26/26).

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

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TokenBasedRememberMeServices::onLoginSuccess can throw a NPE when UserDetailsService::loadUserByUsername returns null.

3 participants