apereo/cas · warning · AccountPasswordMustChangeException
Password has expired
Error message
Password has expired
What it means
The handler throws AccountPasswordMustChangeException('Password has expired') when the boolean/flag column named by properties.getExpiredFieldName() in the query result row is truthy ('true', 'yes', '1', etc.). The credential itself was correct; the account's password is flagged expired and the user must change it.
Solutions
- Have the user change their password so the expired flag clears
- Confirm expiredFieldName matches the intended schema column; if the column is always set, fix the mapping
- If expiry is unexpected, update the flag to false/0 in the database
- Wire an account-status handling flow (e.g. password management) instead of plain rejection
Example fix
// before // cas.authn.jdbc.encode[0].fieldExpired=expired // after (column actually stores last-change date, not a flag) // cas.authn.jdbc.encode[0].fieldExpired=password_expired
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check expiry outside CAS
boolean expired = jdbc.queryForObject("SELECT password_expired FROM users WHERE username=?", Boolean.class, user);
if (expired) forcePasswordChangeFlow(user); Try / catch
try {
authResult = handler.authenticate(credential);
} catch (AccountPasswordMustChangeException e) {
// credential was valid; route to password-change flow
return redirectToPasswordChange(user);
} Prevention
- Map fieldExpired/fieldExpired to the correct expiry-flag column
- Automate password-rotation so flags clear before expiry
- Distinguish AccountPasswordMustChangeException from FailedLoginException in the UI flow
- Audit who/what sets the expired flag
When it happens
Trigger: SQL query returns a row whose expired-flag column (configurable expiredFieldName) parses as BooleanUtils.toBoolean(dbExpired) or equals "1".
Common situations: Database password-aging columns (e.g. pwd_changed or expired) set by an admin or policy job, schema uses 1/0 tinyint flags, misconfigured column accidentally points at a flag that is always 1.
Related errors
- Account has been disabled
- Account has been disabled
- Principal attribute [
- [e.getMessage()]
- Password does not match value on record.
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/30d1d9ea2e826f8b.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-jdbc-authentication/src/main/java/org/apereo/cas/jdbc/QueryAndEncodeDatabaseAuthenticationHandler.java:63
super(properties, principalFactory, dataSource);
this.databasePasswordEncoder = databasePasswordEncoder;
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
final UsernamePasswordCredential transformedCredential, final String originalPassword) throws Throwable {
val username = transformedCredential.getUsername();
try {
val sqlQueryResults = performSqlQuery(username);
val digestedPassword = databasePasswordEncoder.encode(transformedCredential.toPassword(), sqlQueryResults);
if (!sqlQueryResults.get(properties.getPasswordFieldName()).equals(digestedPassword)) {
throw new FailedLoginException("Password does not match value on record.");
}
if (StringUtils.isNotBlank(properties.getExpiredFieldName()) && sqlQueryResults.containsKey(properties.getExpiredFieldName())) {
val dbExpired = sqlQueryResults.get(properties.getExpiredFieldName()).toString();
if (BooleanUtils.toBoolean(dbExpired) || "1".equals(dbExpired)) {
throw new AccountPasswordMustChangeException("Password has expired");
}
}
if (StringUtils.isNotBlank(properties.getDisabledFieldName()) && sqlQueryResults.containsKey(properties.getDisabledFieldName())) {
val dbDisabled = sqlQueryResults.get(properties.getDisabledFieldName()).toString();
if (BooleanUtils.toBoolean(dbDisabled) || "1".equals(dbDisabled)) {
throw new AccountDisabledException("Account has been disabled");
}
}
val attributes = collectPrincipalAttributes(sqlQueryResults);
val principal = principalFactory.createPrincipal(username, attributes);
return createHandlerResult(transformedCredential, principal, new ArrayList<>());
} catch (final IncorrectResultSizeDataAccessException e) {
if (e.getActualSize() == 0) {
throw new AccountNotFoundException(username + " not found with SQL query");
}
throw new FailedLoginException("Multiple records found for " + username);
} catch (final DataAccessException e) {
throw new PreventedException(e);View on GitHub (pinned to e7288fc434)