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

  1. Configure CAS password management for Syncope (cas.authn.pwdmgmt.syncope.*) so the mustChangePassword condition routes to the change-password flow
  2. Have the user change their password in Syncope (self-service console) or admin clears the mustChangePassword flag
  3. Adjust Syncope password policy/reset token expiration if it expires too aggressively
  4. 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

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


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)