apereo/cas · warning · AccountPasswordMustChangeException

Account password must change for

Error message

Account password must change for ${username}

What it means

RestAuthenticationHandler throws AccountPasswordMustChangeException when the remote REST endpoint replies HTTP 428 PRECONDITION_REQUIRED. CAS interprets this as a mandatory password change, typically steering the user into the password-management flow instead of granting a session.

Solutions

  1. Direct the user through CAS password change/reset to clear the flag
  2. Clear the must-change status in the remote system after the password is updated
  3. Verify the endpoint only returns 428 for genuine must-change-password cases
  4. Ensure CAS password management is configured so users can actually complete the change

Example fix

// before: remote account
{"user":"jdoe","forcePasswordChange":true}
// after reset completed
{"user":"jdoe","forcePasswordChange":false}
Defensive patterns

Strategy: try-catch

Validate before calling

val acct = remoteUserStore.lookup(username);
if (acct != null && acct.isForcePasswordChange()) {
    // redirect to password management before authentication
}

Type guard

boolean isPreconditionRequired(HttpResponse r) { return r != null && r.getCode() == 428; }

Try / catch

try {
    return restHandler.authenticate(credential);
} catch (AccountPasswordMustChangeException e) {
    // steer user into CAS password-change flow
    throw e;
}

Prevention

When it happens

Trigger: authenticateUsernamePasswordInternal gets HTTP 428 from the endpoint; the switch maps PRECONDITION_REQUIRED -> AccountPasswordMustChangeException.

Common situations: Remote system flagged the password as expired-but-changeable; admin forced a password reset; the endpoint incorrectly returns 428 for generic precondition issues, misread as must-change-password.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/1f404184a167d64d. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-rest-authentication/src/main/java/org/apereo/cas/adaptors/rest/RestAuthenticationHandler.java:98

            val exec = HttpExecutionRequest
                .builder()
                .basicAuthUsername(credential.getUsername())
                .basicAuthPassword(credential.toPassword())
                .method(HttpMethod.valueOf(properties.getMethod().toUpperCase(Locale.ENGLISH)))
                .url(SpringExpressionLanguageValueResolver.getInstance().resolve(properties.getUri()))
                .httpClient(httpClient)
                .build()
                .withoutRetry();
            response = HttpUtils.execute(exec);
            val status = HttpStatus.resolve(Objects.requireNonNull(response).getCode());
            return switch (Objects.requireNonNull(status)) {
                case OK -> buildPrincipalFromResponse(credential, response);
                case FORBIDDEN -> throw new AccountDisabledException("Could not authenticate forbidden account for " + credential.getUsername());
                case UNAUTHORIZED -> throw new FailedLoginException("Could not authenticate account for " + credential.getUsername());
                case NOT_FOUND -> throw new AccountNotFoundException("Could not locate account for " + credential.getUsername());
                case LOCKED -> throw new AccountLockedException("Could not authenticate locked account for " + credential.getUsername());
                case PRECONDITION_FAILED -> throw new AccountExpiredException("Could not authenticate expired account for " + credential.getUsername());
                case PRECONDITION_REQUIRED -> throw new AccountPasswordMustChangeException("Account password must change for " + credential.getUsername());
                default -> throw new FailedLoginException("Rest endpoint returned an unknown status code " + status + " for " + credential.getUsername());
            };
        } finally {
            HttpUtils.close(response);
        }
    }

    protected AuthenticationHandlerExecutionResult buildPrincipalFromResponse(
        final UsernamePasswordCredential credential,
        final HttpResponse response) throws Throwable {
        try {
            try (val content = ((HttpEntityContainer) response).getEntity().getContent()) {
                val result = IOUtils.toString(content, StandardCharsets.UTF_8);
                LOGGER.debug("REST authentication response received: [{}]", result);
                val principalFromRest = MAPPER.readValue(result, Principal.class);
                val principal = principalFactory.createPrincipal(principalFromRest.getId(), principalFromRest.getAttributes());
                return createHandlerResult(credential, principal, getWarnings(response));
            }

View on GitHub (pinned to e7288fc434)