apereo/cas · warning · AccountPasswordMustChangeException

Password has expired

Error message

Password has expired

What it means

QueryDatabaseAuthenticationHandler checks an 'expired password' column from the SQL result row; if it is truthy (boolean true, '1', etc.), authentication is aborted with AccountPasswordMustChangeException. This signals the credential is correct but the account's password is flagged as expired and must be changed before login.

Solutions

  1. Have the user change their password so the expired flag clears, then retry.
  2. Verify the fieldExpired property points at the correct column and that stale rows are updated (UPDATE users SET expired=0).
  3. If expiry should be handled rather than blocked, adjust cas.authn.jdbc.query[].passwordPolicy settings or clear the column before authentication.
  4. Check BooleanUtils semantics: values like 'true','on','y','1' all count as expired; normalize the column contents.

Example fix

// before: fieldExpired=account_status (holds 1 for many users)
// after: point at the dedicated expiry column and clear stale flags
cas.authn.jdbc.query[0].fieldExpired=password_expired
UPDATE users SET password_expired=0 WHERE password_expired=1 AND password_last_changed < NOW() - INTERVAL '90 days';
Defensive patterns

Strategy: validation

Validate before calling

// Check the expiry flag for the user before calling authenticate
boolean expired = jdbcTemplate.queryForObject(
    "SELECT COALESCE(password_expired,0) FROM users WHERE username=?", Integer.class, username) == 1;
if (expired) { forcePasswordChangeFlow(); }

Try / catch

// catch the specific CAS exception
try { handlerResult = authHandler.authenticate(credential); }
catch (AccountPasswordMustChangeException e) { redirectToPasswordChange(); }

Prevention

When it happens

Trigger: cas.authn.jdbc.query[].fieldExpired is configured and the row returned by sqlFindByUsername contains that column with value 'true'/'1'/'yes' or true.

Common situations: Database password-expiry flag set by DBA policy; misconfigured fieldExpired pointing at an always-true column; schema migration leaving 1 in an expiry column for legacy users.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-jdbc-authentication/src/main/java/org/apereo/cas/jdbc/QueryDatabaseAuthenticationHandler.java:92

                        + username + " or value not parseable as a number");
                }

                val number = NumberUtils.createNumber(count.toString());
                if (number.longValue() != 1) {
                    throw new FailedLoginException("No records found for user " + username);
                }
            }

            if (StringUtils.isNotBlank(properties.getFieldDisabled()) && dbFields.containsKey(properties.getFieldDisabled())) {
                val dbDisabled = dbFields.get(properties.getFieldDisabled()).toString();
                if (BooleanUtils.toBoolean(dbDisabled) || "1".equals(dbDisabled)) {
                    throw new AccountDisabledException("Account has been disabled");
                }
            }
            if (StringUtils.isNotBlank(properties.getFieldExpired()) && dbFields.containsKey(properties.getFieldExpired())) {
                val dbExpired = dbFields.get(properties.getFieldExpired()).toString();
                if (BooleanUtils.toBoolean(dbExpired) || "1".equals(dbExpired)) {
                    throw new AccountPasswordMustChangeException("Password has expired");
                }
            }

            val attributes = collectPrincipalAttributes(dbFields);
            val principal = this.principalFactory.createPrincipal(username, attributes);
            return createHandlerResult(credential, 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);
        }
    }

    protected Map<String, Object> query(final UsernamePasswordCredential credential) {

View on GitHub (pinned to e7288fc434)