spring-projects/spring-security · error · LockedException

User account is locked

Error message

User account is locked

What it means

ActiveDirectoryLdapAuthenticationProvider maps AD bind-failure sub-codes to Spring Security exceptions. When AD bind fails with sub-error 775 (ACCOUNT_LOCKED, too many bad password attempts), raiseExceptionForErrorCode throws LockedException('User account is locked') wrapping the ActiveDirectoryAuthenticationException as cause.

Source

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

		}
		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. Wait for the AD lockout duration to pass or have an admin unlock the account (Unlock-ADAccount).
  2. Fix the stale/incorrect credentials in any application or scheduled job using this account to stop re-locking.
  3. Catch LockedException separately to inform the user their account is locked rather than 'wrong password'.
  4. Review AD lockout policy and the security event log to find the source of bad password attempts.

Example fix

// before
catch (BadCredentialsException e) { return "login?error"; }
// after
catch (LockedException e) { model.addAttribute("msg", "Account locked; contact helpdesk"); return "login"; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
    return authenticationManager.authenticate(token);
} catch (LockedException e) {
    model.addAttribute("msg", "Account locked; contact helpdesk or retry later");
    return "login";
}

Prevention

When it happens

Trigger: authenticate() -> handleBindException() -> raiseExceptionForErrorCode(ACCOUNT_LOCKED), raised when the LDAP bind against AD fails with sub-error code 775 indicating the account is locked out by the domain lockout policy.

Common situations: Repeated wrong-password logins triggering AD lockout policy; a service with stale credentials retrying continuously and locking the account; user locked out across the domain by attacker lockout or misconfigured app.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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