spring-projects/spring-security · error · CredentialsExpiredException

AbstractUserDetailsAuthenticationProvider.credentialsExpired

AbstractUserDetailsAuthenticationProvider.credentialsExpired

Error message

User credentials have expired

What it means

defaultPostAuthenticationChecks in AbstractUserDetailsReactiveAuthenticationManager runs after successful password verification and throws CredentialsExpiredException with 'User credentials have expired' (message code AbstractUserDetailsAuthenticationProvider.credentialsExpired) when isCredentialsNonExpired() returns false. The password was correct, but the credential's lifetime has ended, so authentication still fails.

Source

Thrown at core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java:95

			throw new LockedException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
					"User account is locked"));
		}
		if (!user.isEnabled()) {
			this.logger.debug("User account is disabled");
			throw new DisabledException(
					this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
		}
		if (!user.isAccountNonExpired()) {
			this.logger.debug("User account is expired");
			throw new AccountExpiredException(this.messages
				.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
		}
	}

	private void defaultPostAuthenticationChecks(UserDetails user) {
		if (!user.isCredentialsNonExpired()) {
			this.logger.debug("User account credentials have expired");
			throw new CredentialsExpiredException(this.messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
		}
	}

	@Override
	public Mono<Authentication> authenticate(Authentication authentication) {
		String username = authentication.getName();
		String presentedPassword = (authentication.getCredentials() != null)
				? authentication.getCredentials().toString() : null;
		// @formatter:off
		return retrieveUser(username)
				.doOnNext(this.preAuthenticationChecks::check)
				.publishOn(this.scheduler)
				.filter((userDetails) -> this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()))
				.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
				.flatMap((userDetails) -> checkCompromisedPassword(presentedPassword).thenReturn(userDetails))
				.flatMap((userDetails) -> upgradeEncodingIfNecessary(userDetails, presentedPassword))
				.doOnNext(this.postAuthenticationChecks::check)

View on GitHub (pinned to 96852e8860)

Solutions

  1. Return true from isCredentialsNonExpired() or reset the credential-expiry timestamp after a password change
  2. Direct the user to a change-password flow when catching CredentialsExpiredException
  3. Update passwordLastChanged on every password reset so expiry is computed from the right date
  4. Verify your UserDetails implementation overrides isCredentialsNonExpired() — the interface default is false

Example fix

// before
@Override public boolean isCredentialsNonExpired() { return false; }
// after
@Override public boolean isCredentialsNonExpired() {
  return passwordChangedAt.plusDays(90).isAfter(Instant.now());
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!userDetails.isCredentialsNonExpired()) throw new CredentialsExpiredException("Password expired for " + username);

Type guard

boolean passwordCurrent(UserDetails u) { return u.isCredentialsNonExpired(); }

Try / catch

authManager.authenticate(token)
  .onErrorResume(CredentialsExpiredException.class, e -> redirectToChangePassword());

Prevention

When it happens

Trigger: authenticate() where the UserDetails has correct credentials but isCredentialsNonExpired() returns false — e.g. password-rotation policies marking credentials stale, or custom UserDetails not overriding isCredentialsNonExpired() (default false).

Common situations: Corporate password-aging policies (90-day rotation); passwords flagged for mandatory reset; custom UserDetails implementations missing the override; CI/test credentials older than the rotation window.

Related errors


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