spring-projects/spring-security · error · CredentialsExpiredException

User credentials have expired

Error message

User credentials have expired

What it means

ActiveDirectoryLdapAuthenticationProvider maps AD bind-failure sub-error codes to Spring Security exceptions. When AD returns error 49 (data 532) meaning the user's password has expired, raiseExceptionForErrorCode throws CredentialsExpiredException with message 'User credentials have expired', wrapping the ActiveDirectoryAuthenticationException as the cause.

Source

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

		}
	}

	private int parseSubErrorCode(@Nullable String message) {
		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";

View on GitHub (pinned to 96852e8860)

Solutions

  1. The user must change their AD password (via Ctrl+Alt+Del on a domain machine, self-service portal, or admin reset).
  2. For service accounts, rotate the configured password and update the application's credentials.
  3. Admin can set the password to not expire for the account if appropriate (Set-AdUser -PasswordNeverExpires).
  4. Catch CredentialsExpiredException in your failure handler and redirect users to a password-change flow.

Example fix

// before: generic failure handler
catch (BadCredentialsException e) { return "login?error"; }
// after: handle expired credentials distinctly
catch (CredentialsExpiredException e) { return "redirect:/password/change-expired"; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
    return authenticationManager.authenticate(token);
} catch (CredentialsExpiredException e) {
    return "redirect:/password/expired"; // send user to change-password flow
}

Prevention

When it happens

Trigger: authenticate() -> handleBindException() -> raiseExceptionForErrorCode(PASSWORD_EXPIRED), triggered when an LDAP bind against Active Directory fails with error code 532 (ERROR_PASSWORD_EXPIRED) during bindAuthentication.

Common situations: AD domain password policy's max password age elapsed for the user; service accounts whose passwords were not rotated; users authenticating who must change their password at next logon after expiration.

Related errors


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