spring-projects/spring-security · error · AccountExpiredException

User account has expired

Error message

User account has expired

What it means

ActiveDirectoryLdapAuthenticationProvider maps AD bind-failure sub-codes to Spring Security exceptions. When AD bind fails with sub-error 701 (ACCOUNT_EXPIRED, account validity elapsed), raiseExceptionForErrorCode throws AccountExpiredException('User account has expired') wrapping the ActiveDirectoryAuthenticationException as cause.

Source

Thrown at ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java:269

		if (message == null) {
			return -1;
		}
		Matcher matcher = SUB_ERROR_CODE.matcher(message);
		if (matcher.matches()) {
			return Integer.parseInt(matcher.group(1), 16);
		}
		return -1;
	}

	private void raiseExceptionForErrorCode(int code, NamingException exception) {
		String hexString = Integer.toHexString(code);
		Throwable cause = new ActiveDirectoryAuthenticationException(hexString, exception.getMessage(), exception);
		switch (code) {
			case PASSWORD_EXPIRED -> throw new CredentialsExpiredException(this.messages
				.getMessage("LdapAuthenticationProvider.credentialsExpired", "User credentials have expired"), cause);
			case ACCOUNT_DISABLED -> throw new DisabledException(
					this.messages.getMessage("LdapAuthenticationProvider.disabled", "User is disabled"), cause);
			case ACCOUNT_EXPIRED -> throw new AccountExpiredException(
					this.messages.getMessage("LdapAuthenticationProvider.expired", "User account has expired"), cause);
			case ACCOUNT_LOCKED -> throw new LockedException(
					this.messages.getMessage("LdapAuthenticationProvider.locked", "User account is locked"), cause);
			default -> throw badCredentials(cause);
		}
	}

	private String subCodeToLogMessage(int code) {
		return switch (code) {
			case USERNAME_NOT_FOUND -> "User was not found in directory";
			case INVALID_PASSWORD -> "Supplied password was invalid";
			case NOT_PERMITTED -> "User not permitted to logon at this time";
			case PASSWORD_EXPIRED -> "Password has expired";
			case ACCOUNT_DISABLED -> "Account is disabled";
			case ACCOUNT_EXPIRED -> "Account expired";
			case PASSWORD_NEEDS_RESET -> "User must reset password";
			case ACCOUNT_LOCKED -> "Account locked";
			default -> "Unknown (error code " + Integer.toHexString(code) + ")";

View on GitHub (pinned to 96852e8860)

Solutions

  1. Ask an AD administrator to extend or clear the account expiration date (Set-ADAccountExpiration or set accountExpires to never).
  2. Confirm the correct account is being used.
  3. Catch AccountExpiredException in your auth failure handler to surface an account-expired message.
  4. For service accounts, set the account to never expire and control access via groups instead.

Example fix

// before
catch (BadCredentialsException e) { return "login?error"; }
// after
catch (AccountExpiredException e) { return "redirect:/account/expired"; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
    return authenticationManager.authenticate(token);
} catch (AccountExpiredException e) {
    return "redirect:/account/expired";
}

Prevention

When it happens

Trigger: authenticate() -> handleBindException() -> raiseExceptionForErrorCode(ACCOUNT_EXPIRED), raised when the LDAP bind against AD fails with sub-error code 701 indicating the account's accountExpires date has passed.

Common situations: Temporary/contractor accounts with an accountExpires date that elapsed; accounts created with an expiration window never extended; seasonal accounts auto-expiring per policy.

Related errors


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