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
- Direct the user through CAS password change/reset to clear the flag
- Clear the must-change status in the remote system after the password is updated
- Verify the endpoint only returns 428 for genuine must-change-password cases
- 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
- Ensure CAS password management is enabled and reachable
- Clear must-change flags automatically once the password change completes
- Keep 428 usage exclusive to must-change-password in the endpoint
- Notify users before forced-change deadlines
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
- Unable to accept response status
- No credentials can be extracted to authenticate the REST…
- Unable to extract credentials for multifactor authentication
- Could not authenticate forbidden account for
- Could not authenticate account for
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)