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

  1. Extend the account's expiry date in the user store or set isAccountNonExpired() to return true if expiry is not intended
  2. Implement renewal workflow so expired accounts are re-validated and extended
  3. If you don't use account expiry, always return true from isAccountNonExpired() in your UserDetails
  4. 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

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


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