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
- Return true from isCredentialsNonExpired() or reset the credential-expiry timestamp after a password change
- Direct the user to a change-password flow when catching CredentialsExpiredException
- Update passwordLastChanged on every password reset so expiry is computed from the right date
- 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
- Update passwordChangedAt on every password change
- Return true from isCredentialsNonExpired() unless a rotation policy applies
- Send proactive password-expiry notifications
- Override isCredentialsNonExpired() in custom UserDetails (default false)
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
- AbstractUserDetailsAuthenticationProvider.locked
- AbstractUserDetailsAuthenticationProvider.disabled
- AbstractUserDetailsAuthenticationProvider.expired
- The provided password is compromised, please change your pas
- ${ex.getStatus().getDefaultMessage()}
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/dc3fe829082e3bfe.
Report an issue: GitHub.