spring-projects/spring-security · error · AccountExpiredException
AccountStatusUserDetailsChecker.expired
AccountStatusUserDetailsChecker.expired
Error message
User account has expired
What it means
AccountStatusUserDetailsChecker.check() throws AccountExpiredException with message 'User account has expired' when UserDetails.isAccountNonExpired() returns false. The account exists and is enabled but its validity period has ended.
Source
Thrown at core/src/main/java/org/springframework/security/authentication/AccountStatusUserDetailsChecker.java:56
private final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
this.logger.debug("Failed to authenticate since user account is locked");
throw new LockedException(
this.messages.getMessage("AccountStatusUserDetailsChecker.locked", "User account is locked"));
}
if (!user.isEnabled()) {
this.logger.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(
this.messages.getMessage("AccountStatusUserDetailsChecker.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
this.logger.debug("Failed to authenticate since user account is expired");
throw new AccountExpiredException(
this.messages.getMessage("AccountStatusUserDetailsChecker.expired", "User account has expired"));
}
if (!user.isCredentialsNonExpired()) {
this.logger.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(this.messages
.getMessage("AccountStatusUserDetailsChecker.credentialsExpired", "User credentials have expired"));
}
}
/**
* Sets the {@link MessageSource} used to resolve exception messages.
* @since 5.2
*/
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}View on GitHub (pinned to 96852e8860)
Solutions
- Extend the account's expiry date in the user store or set isAccountNonExpired() to return true if expiry is not intended
- Implement renewal workflow so expired accounts are re-validated and extended
- If you don't use account expiry, always return true from isAccountNonExpired() in your UserDetails
- Catch AccountExpiredException in your failure handler to render an 'account expired, contact support' message
Example fix
// before
@Override
public boolean isAccountNonExpired() { return LocalDate.now().isBefore(expiryDate); } // expired
// after
@Override
public boolean isAccountNonExpired() { return expiryDate == null || LocalDate.now().isBefore(expiryDate); } Defensive patterns
Strategy: try-catch
Validate before calling
UserDetails user = uds.loadUserByUsername(username);
if (!user.isAccountNonExpired()) { throw new IllegalStateException("Account expired: " + username); } Type guard
boolean isLoginAllowed(UserDetails u) { return u.isAccountNonExpired(); } Try / catch
try { authMgr.authenticate(token); } catch (AccountExpiredException e) { return ResponseEntity.status(403).body("Account expired. Please renew."); } Prevention
- Model expiry as an optional/nullable date so unused expiry never rejects users
- Alert users before their account expiry date
- Unit-test UserDetails implementations for all four status flags
When it happens
Trigger: Authentication of a UserDetails whose isAccountNonExpired() returns false, typically checked during DaoAuthenticationProvider's post-authentication checks.
Common situations: Accounts with a fixed contract/lifetime (expiryDate in DB); loading user with expired-by-date flag; custom UserDetails hardcoding accountNonExpired=false; time-based account policies in enterprise systems.
Related errors
- User account has expired
- RunAsImplAuthenticationProvider.incorrectKey
- Authenticated principal required to operate with ACLs
- CasAuthenticationProvider.incorrectKey
- oidc_provider_not_configured
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/c282e1d318840538.
Report an issue: GitHub.