spring-projects/spring-security · error · BadCredentialsException
Failed to authenticate the one-time token
Error message
Failed to authenticate the one-time token
What it means
OneTimeTokenAuthenticationProvider catches UsernameNotFoundException from userDetailsService.loadUserByUsername(consumed.getUsername()) and rethrows BadCredentialsException('Failed to authenticate the one-time token'). The opaque message prevents attackers from discovering which usernames exist. This happens when the token is valid but no user account matches the username stored with the token.
Source
Thrown at core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationProvider.java:80
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OneTimeTokenAuthenticationToken otpAuthenticationToken = (OneTimeTokenAuthenticationToken) authentication;
OneTimeToken consumed = this.oneTimeTokenService.consume(otpAuthenticationToken);
if (consumed == null) {
throw new InvalidOneTimeTokenException("Invalid token");
}
try {
UserDetails user = this.userDetailsService.loadUserByUsername(consumed.getUsername());
this.userDetailsChecker.check(user);
Collection<GrantedAuthority> authorities = new HashSet<>(user.getAuthorities());
authorities.add(FactorGrantedAuthority.fromAuthority(AUTHORITY));
OneTimeTokenAuthentication authenticated = new OneTimeTokenAuthentication(user, authorities);
authenticated.setDetails(otpAuthenticationToken.getDetails());
return authenticated;
}
catch (UsernameNotFoundException ex) {
throw new BadCredentialsException("Failed to authenticate the one-time token");
}
}
@Override
public boolean supports(Class<?> authentication) {
return OneTimeTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* Use this {@link UserDetailsChecker} to verify the status of the loaded
* {@link UserDetails} after authentication.
*
* <p>
* By default, no checks are performed, keeping this provider's behavior consistent
* with earlier versions of Spring Security. To reject authentication for accounts
* that are locked, disabled, or expired, provide a
* {@link AccountStatusUserDetailsChecker}.
* @param userDetailsChecker the {@link UserDetailsChecker} to useView on GitHub (pinned to 96852e8860)
Solutions
- Verify the username exists and the account is enabled before generating the one-time token (silently skip or handle unknown users to avoid token spam)
- Catch BadCredentialsException in the OTT failure handler and show a generic error directing the user to request a new token or use another login method
- Check that the configured UserDetailsService is the one holding the target users (correct tenant/realm/user registry)
- Confirm case-sensitivity: normalize usernames (e.g. lowercase emails) both at token generation and lookup
Example fix
// before
oneTimeTokenService.generate(new GenerateOneTimeTokenRequest(username)); // username may not exist
// after
if (userDetailsService instanceof UserDetailsManager m && m.userExists(username)) {
oneTimeTokenService.generate(new GenerateOneTimeTokenRequest(username));
} Defensive patterns
Strategy: try-catch
Validate before calling
if (username == null || username.isBlank()) { throw new IllegalArgumentException("username required to generate one-time token"); } Try / catch
try { return authManager.authenticate(ottToken); } catch (BadCredentialsException e) { log.debug("OTT login failed: unknown or invalid user"); return redirect("/login?ottFailed=1"); } Prevention
- Normalize usernames (trim/lowercase emails) at both token generation and UserDetailsService lookup
- Ensure the OTT request page cannot be abused to spam tokens for arbitrary usernames (rate-limit)
- Use the same UserDetailsService for OTT token issuance and authentication
- Keep account lifecycle (deletion/renaming) consistent with outstanding tokens; expire tokens on user deletion
When it happens
Trigger: A one-time token was generated for a username that does not exist (e.g. attacker-supplied or misspelled username on the OTT request form); the user account was deleted or renamed between token generation and consumption; the UserDetailsService bean in use does not include the user (e.g. wrong realm/tenant).
Common situations: OTT login pages that accept arbitrary email input without verifying account existence (by design, to avoid user enumeration); user deactivated between requesting and clicking the login link; multi-tenant apps where the token was issued in a different tenant context.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- RunAsImplAuthenticationProvider.incorrectKey
- CasAuthenticationProvider.incorrectKey
- Bad credentials
- Invalid token
- Bad credentials
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/8ec0ae2cbd1e215a.
Report an issue: GitHub.