apereo/cas · warning · AccountPasswordMustChangeException
Account password must change for
Error message
Account password must change for
What it means
SyncopeAuthenticationHandler.authenticateUsernamePasswordInternal throws AccountPasswordMustChangeException when Syncope's user object has mustChangePassword == true. Syncope enforces a password-change requirement (expired or admin-forced reset), and CAS surfaces it as a special AuthenticationException so the webflow can route the user to a change-password flow instead of logging them in.
Solutions
- Configure CAS password management for Syncope (cas.authn.pwdmgmt.syncope.*) so the mustChangePassword condition routes to the change-password flow
- Have the user change their password in Syncope (self-service console) or admin clears the mustChangePassword flag
- Adjust Syncope password policy/reset token expiration if it expires too aggressively
- Verify CAS version supports the Syncope password management module and it's on the classpath
Example fix
// before cas.authn.syncope.url=https://syncope.example.org/syncope // after cas.authn.syncope.url=https://syncope.example.org/syncope cas.authn.pwdmgmt.syncope.url=https://syncope.example.org/syncope cas.authn.pwdmgmt.syncope.domain=Two
Defensive patterns
Strategy: try-catch
Validate before calling
JsonNode user = fetchSyncopeUser(u, p);
if (user != null && user.path("mustChangePassword").asBoolean(false)) {
redirect("/cas/login?changePassword=true");
} Try / catch
try {
return authenticationManager.authenticate(transaction);
} catch (AccountPasswordMustChangeException e) {
return "casMustChangePasswordView"; // route to pwdmgmt flow
} Prevention
- Configure cas.authn.pwdmgmt.* for Syncope so users get a change-password flow
- Set reasonable password policy / expiry in Syncope
- Provision users with non-expiring initial passwords plus forced-change flag and a working flow
- Test the full change-password round-trip before rollout
When it happens
Trigger: Successful credential lookup where the returned Syncope user JSON contains "mustChangePassword": true; the user then must complete the password-change action before CAS will issue a handler result.
Common situations: Admin set forceChangePassword / pwdPolicy expiring password; password aged out per Syncope password policy; freshly provisioned user with a temporary password; missing CAS password-management (pwdmgmt) configuration so users can never complete the change and stay stuck.
Related errors
- Could not authenticate forbidden account for
- Could not authenticate account for
- Password has expired
- Radius authentication failed for user
- Radius authentication failed for user
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/de8bb8ebb7f9ea7f.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-syncope-authentication/src/main/java/org/apereo/cas/syncope/SyncopeAuthenticationHandler.java:66
final String syncopeDomain) {
super(properties.getName(), principalFactory, properties.getOrder());
this.properties = properties;
this.syncopeDomain = syncopeDomain;
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
final UsernamePasswordCredential credential, @Nullable final String originalPassword) throws Throwable {
val result = authenticateSyncopeUser(credential);
if (result.isPresent()) {
val user = result.get();
LOGGER.debug("Received Syncope user object as [{}]", user);
if (user.has("suspended") && user.get("suspended").asBoolean()) {
throw new AccountDisabledException(
"Could not authenticate forbidden account for " + credential.getUsername());
}
if (user.has("mustChangePassword") && user.get("mustChangePassword").asBoolean()) {
throw new AccountPasswordMustChangeException(
"Account password must change for " + credential.getUsername());
}
val principalAttributes = SyncopeUtils.convertFromUserEntity(user, properties.getAttributeMappings());
val name = properties.getAttributeMappings().getOrDefault("domain", "syncopeDomain");
principalAttributes.put(name, CollectionUtils.wrapList(syncopeDomain));
val principal = principalFactory.createPrincipal(user.get("username").asString(), principalAttributes);
return createHandlerResult(credential, principal, new ArrayList<>());
}
throw new FailedLoginException("Could not authenticate account for " + credential.getUsername());
}
protected Optional<JsonNode> authenticateSyncopeUser(final UsernamePasswordCredential credential) {
HttpResponse response = null;
try {
val syncopeRestUrl = Strings.CI.appendIfMissing(
SpringExpressionLanguageValueResolver.getInstance().resolve(properties.getUrl()),
"/rest/users/self");
val exec = HttpExecutionRequest.builder()View on GitHub (pinned to e7288fc434)