spring-projects/spring-security · error · PasswordPolicyException

${ctrl.getErrorStatus()}

Error message

${ctrl.getErrorStatus()}

What it means

PasswordPolicyAwareContextSource.getContext() consults the LDAP password-policy response control after a failed bind. If the control indicates the account is locked and carries an error status string, it throws PasswordPolicyException with the server's error status as the message; otherwise the original NamingException is converted normally.

Source

Thrown at ldap/src/main/java/org/springframework/security/ldap/ppolicy/PasswordPolicyAwareContextSource.java:69

			return super.getContext(principal, credentials);
		}
		this.logger.trace(LogMessage.format("Binding as %s, prior to reconnect as user %s", getUserDn(), principal));
		// First bind as manager user before rebinding as the specific principal.
		LdapContext ctx = (LdapContext) super.getContext(getUserDn(), getPassword());
		Control[] rctls = { new PasswordPolicyControl(false) };
		try {
			ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal);
			ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
			ctx.reconnect(rctls);
		}
		catch (javax.naming.NamingException ex) {
			PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor.extractControl(ctx);
			if (this.logger.isDebugEnabled()) {
				this.logger.debug(LogMessage.format("Failed to bind with %s", ctrl), ex);
			}
			LdapUtils.closeContext(ctx);
			if (ctrl != null && ctrl.isLocked() && ctrl.getErrorStatus() != null) {
				throw new PasswordPolicyException(ctrl.getErrorStatus());
			}
			throw LdapUtils.convertLdapException(ex);
		}
		this.logger.debug(LogMessage.of(() -> "Bound with " + PasswordPolicyControlExtractor.extractControl(ctx)));
		return ctx;
	}

	@Override
	@SuppressWarnings("unchecked")
	protected Hashtable getAuthenticatedEnv(String principal, String credentials) {
		Hashtable<String, Object> env = super.getAuthenticatedEnv(principal, credentials);
		env.put(LdapContext.CONTROL_FACTORIES, PasswordPolicyControlFactory.class.getName());
		return env;
	}

}

View on GitHub (pinned to 96852e8860)

Solutions

  1. Have the user's account unlocked / password changed according to the directory's password policy.
  2. Read PasswordPolicyException.getMessage() — it contains the server's error status describing the exact policy violation.
  3. Catch PasswordPolicyException to route users to unlock/reset flows instead of showing 'wrong password'.
  4. Check ppolicy overlay configuration (lockout duration, pwdMaxAge) if lockouts occur unexpectedly.

Example fix

// before
catch (BadCredentialsException e) { return "login?error"; }
// after
catch (PasswordPolicyException e) { model.addAttribute("msg", e.getMessage()); return "redirect:/account/unlock"; }
Defensive patterns

Strategy: try-catch

Try / catch

try {
    DirContext ctx = passwordPolicyAwareContextSource.getContext(userDn, password);
} catch (PasswordPolicyException e) {
    // e.getMessage() contains the server error status (locked/expired)
    model.addAttribute("policyMessage", e.getMessage());
}

Prevention

When it happens

Trigger: Calling getContext(username, password) (directly or via LDAP authentication using this ContextSource) where the bind fails, the server returns a PasswordPolicyResponseControl with isLocked()==true and non-null error status, e.g. 'account is locked' or 'password expired'.

Common situations: OpenLDAP/Oracle directory with ppolicy overlay locking accounts after failed attempts; authenticating a user whose password is expired per password policy; password must-change state preventing bind.

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/9995188f3d2876cd. Report an issue: GitHub.