spring-projects/spring-security · error · BadCredentialsException
Authentication for password change failed.
Error message
Authentication for password change failed.
What it means
LdapUserDetailsManager.changePassword() with usePasswordModifyExtensionOperation=false performs the change by reconnecting the context with the old credentials and then modifying the password attribute. If the reconnect raises javax.naming.AuthenticationException, the old password was wrong, and changePasswordUsingAttributeModification throws BadCredentialsException('Authentication for password change failed.').
Solutions
- Have the user re-enter and confirm their current (old) password — it must authenticate successfully first.
- Verify the context's SECURITY_PRINCIPAL is the user's own DN, not a shared/admin identity with different credentials.
- Catch BadCredentialsException in the change-password flow and prompt for the current password again.
- If policy complexities exist, consider enabling the password modify extended operation (setPasswordModifyExtensionOperation(true)).
Example fix
// before
manager.changePassword(oldPassword, newPassword); // opaque failure
// after
try {
manager.changePassword(oldPassword, newPassword);
}
catch (BadCredentialsException e) {
bindingResult.rejectValue("oldPassword", "wrong.current.password");
return "password/change";
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check old password with a bind attempt
try (DirContext c = contextSource.getContext(userDn, oldPassword)) {
// old password valid; proceed to changePassword
} Try / catch
try {
ldapUserDetailsManager.changePassword(oldPassword, newPassword);
} catch (BadCredentialsException e) {
bindingResult.rejectValue("oldPassword", "wrong.current.password");
} Prevention
- Confirm the current password with a bind test before the change flow.
- Ensure SECURITY_PRINCIPAL is the user's own DN, not a shared identity.
- Catch BadCredentialsException explicitly in change-password forms.
- Consider RFC 3062 extended operation for directories where reconnect semantics are flaky.
When it happens
Trigger: Calling changePassword(oldPassword, newPassword) where reconnect(null) with SECURITY_CREDENTIALS=oldPassword fails with javax.naming.AuthenticationException — i.e. the supplied old password does not authenticate against the directory.
Common situations: User typed their current password incorrectly on a change-password form; session was re-bound with admin credentials so the reconnect uses the wrong identity; directory rejects the bind due to policy (locked/expired) mid-change.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Bad credentials
- Authentication.getCredentials() cannot be null
- Bad credentials
- Bad credentials
- Bad credentials
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/49093c2eca7e6b66.
Report an issue: GitHub.
Appendix: source
Thrown at ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapUserDetailsManager.java:453
private void changePasswordUsingAttributeModification(LdapName userDn, @Nullable String oldPassword,
@Nullable String newPassword) {
ModificationItem[] passwordChange = new ModificationItem[] { new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(this.passwordAttributeName, newPassword)) };
if (oldPassword == null) {
this.template.modifyAttributes(userDn, passwordChange);
return;
}
this.template.executeReadWrite((dirCtx) -> {
LdapContext ctx = (LdapContext) dirCtx;
ctx.removeFromEnvironment("com.sun.jndi.ldap.connect.pool");
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, LdapUtils.getFullDn(userDn, ctx).toString());
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, oldPassword);
// TODO: reconnect doesn't appear to actually change the credentials
try {
ctx.reconnect(null);
}
catch (javax.naming.AuthenticationException ex) {
throw new BadCredentialsException("Authentication for password change failed.");
}
ctx.modifyAttributes(userDn, passwordChange);
return void.class;
});
}
private void changePasswordUsingExtensionOperation(LdapName userDn, @Nullable String oldPassword,
@Nullable String newPassword) {
this.template.executeReadWrite((dirCtx) -> {
LdapContext ctx = (LdapContext) dirCtx;
String userIdentity = LdapUtils.getFullDn(userDn, ctx).toString();
PasswordModifyRequest request = new PasswordModifyRequest(userIdentity, oldPassword, newPassword);
try {
return ctx.extendedOperation(request);
}
catch (javax.naming.AuthenticationException ex) {
throw new BadCredentialsException("Authentication for password change failed.");
}View on GitHub (pinned to 96852e8860)