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

  1. Have the user change their password so the expired flag clears
  2. Confirm expiredFieldName matches the intended schema column; if the column is always set, fix the mapping
  3. If expiry is unexpected, update the flag to false/0 in the database
  4. 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

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


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)