apereo/cas · warning · AccountPasswordMustChangeException

AccountPasswordMustChangeException

Error message

AccountPasswordMustChangeException

What it means

RedisAuthenticationHandler throws AccountPasswordMustChangeException when the Redis account record has status MUST_CHANGE_PASSWORD. CAS signals that authentication may proceed administratively but the user must change their password before being fully admitted, typically routing the flow to a password-change screen.

Solutions

  1. Have the user complete the CAS password-change flow to clear the must-change flag
  2. Reset/clear the MUST_CHANGE_PASSWORD status in the Redis account record once the password is updated
  3. Check the provisioning source that sets this status if it was set unintentionally

Example fix

// before (stored in Redis)
{"username":"jdoe","status":"MUST_CHANGE_PASSWORD"}
// after password change
{"username":"jdoe","status":"OK"}
Defensive patterns

Strategy: try-catch

Validate before calling

val acct = redisAccountService.findAccount(username);
if (acct != null && acct.getStatus() == AccountStatus.MUST_CHANGE_PASSWORD) {
    // redirect to password change flow instead of authenticating
}

Type guard

boolean needsPasswordChange(AccountState s) {
    return s != null && s.getStatus() == AccountStatus.MUST_CHANGE_PASSWORD;
}

Try / catch

try {
    return authenticationHandler.authenticate(credential);
} catch (AccountPasswordMustChangeException e) {
    // send user to the password-change webflow
    throw e;
}

Prevention

When it happens

Trigger: authenticateUsernamePasswordInternal loads the account from Redis, the password matches, but account.getStatus() is MUST_CHANGE_PASSWORD in the status switch.

Common situations: A password expired or an admin forced a reset flag in the Redis user store; an external provisioning system marked the account must-change; user was migrated with a forced-reset flag set.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-redis-authentication/src/main/java/org/apereo/cas/redis/RedisAuthenticationHandler.java:47

    }

    @Override
    protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
        final UsernamePasswordCredential credential,
        final String originalPassword) throws Throwable {
        val account = (RedisUserAccount) redisTemplate.opsForValue().get(credential.getUsername());
        if (account == null) {
            throw new AccountNotFoundException();
        }
        if (!getPasswordEncoder().matches(originalPassword, account.getPassword())) {
            LOGGER.warn("Account password on record for [{}] does not match the given/encoded password", credential.getId());
            throw new FailedLoginException();
        }
        switch (account.getStatus()) {
            case DISABLED -> throw new AccountDisabledException();
            case EXPIRED -> throw new AccountExpiredException();
            case LOCKED -> throw new AccountLockedException();
            case MUST_CHANGE_PASSWORD -> throw new AccountPasswordMustChangeException();
            case OK -> LOGGER.debug("Account status is OK");
        }
        val principal = principalFactory.createPrincipal(account.getUsername(), account.getAttributes());
        return createHandlerResult(credential, principal, new ArrayList<>());
    }
}

View on GitHub (pinned to e7288fc434)