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 use

View on GitHub (pinned to 96852e8860)

Solutions

  1. 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)
  2. 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
  3. Check that the configured UserDetailsService is the one holding the target users (correct tenant/realm/user registry)
  4. 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

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

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/8ec0ae2cbd1e215a. Report an issue: GitHub.