{"record":{"id":"1f404184a167d64d","repo":"apereo/cas","slug":"account-password-must-change-for-username","errorCode":null,"errorMessage":"Account password must change for ${username}","messagePattern":"Account password must change for (.+?)","errorType":"exception","errorClass":"AccountPasswordMustChangeException","httpStatus":428,"severity":"warning","filePath":"support/cas-server-support-rest-authentication/src/main/java/org/apereo/cas/adaptors/rest/RestAuthenticationHandler.java","lineNumber":98,"sourceCode":"            val exec = HttpExecutionRequest\n                .builder()\n                .basicAuthUsername(credential.getUsername())\n                .basicAuthPassword(credential.toPassword())\n                .method(HttpMethod.valueOf(properties.getMethod().toUpperCase(Locale.ENGLISH)))\n                .url(SpringExpressionLanguageValueResolver.getInstance().resolve(properties.getUri()))\n                .httpClient(httpClient)\n                .build()\n                .withoutRetry();\n            response = HttpUtils.execute(exec);\n            val status = HttpStatus.resolve(Objects.requireNonNull(response).getCode());\n            return switch (Objects.requireNonNull(status)) {\n                case OK -> buildPrincipalFromResponse(credential, response);\n                case FORBIDDEN -> throw new AccountDisabledException(\"Could not authenticate forbidden account for \" + credential.getUsername());\n                case UNAUTHORIZED -> throw new FailedLoginException(\"Could not authenticate account for \" + credential.getUsername());\n                case NOT_FOUND -> throw new AccountNotFoundException(\"Could not locate account for \" + credential.getUsername());\n                case LOCKED -> throw new AccountLockedException(\"Could not authenticate locked account for \" + credential.getUsername());\n                case PRECONDITION_FAILED -> throw new AccountExpiredException(\"Could not authenticate expired account for \" + credential.getUsername());\n                case PRECONDITION_REQUIRED -> throw new AccountPasswordMustChangeException(\"Account password must change for \" + credential.getUsername());\n                default -> throw new FailedLoginException(\"Rest endpoint returned an unknown status code \" + status + \" for \" + credential.getUsername());\n            };\n        } finally {\n            HttpUtils.close(response);\n        }\n    }\n\n    protected AuthenticationHandlerExecutionResult buildPrincipalFromResponse(\n        final UsernamePasswordCredential credential,\n        final HttpResponse response) throws Throwable {\n        try {\n            try (val content = ((HttpEntityContainer) response).getEntity().getContent()) {\n                val result = IOUtils.toString(content, StandardCharsets.UTF_8);\n                LOGGER.debug(\"REST authentication response received: [{}]\", result);\n                val principalFromRest = MAPPER.readValue(result, Principal.class);\n                val principal = principalFactory.createPrincipal(principalFromRest.getId(), principalFromRest.getAttributes());\n                return createHandlerResult(credential, principal, getWarnings(response));\n            }","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-rest-authentication/src/main/java/org/apereo/cas/adaptors/rest/RestAuthenticationHandler.java#L80-L116","documentation":"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.","triggerScenarios":"authenticateUsernamePasswordInternal gets HTTP 428 from the endpoint; the switch maps PRECONDITION_REQUIRED -> AccountPasswordMustChangeException.","commonSituations":"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.","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"],"exampleFix":"// before: remote account\n{\"user\":\"jdoe\",\"forcePasswordChange\":true}\n// after reset completed\n{\"user\":\"jdoe\",\"forcePasswordChange\":false}","handlingStrategy":"try-catch","validationCode":"val acct = remoteUserStore.lookup(username);\nif (acct != null && acct.isForcePasswordChange()) {\n    // redirect to password management before authentication\n}","typeGuard":"boolean isPreconditionRequired(HttpResponse r) { return r != null && r.getCode() == 428; }","tryCatchPattern":"try {\n    return restHandler.authenticate(credential);\n} catch (AccountPasswordMustChangeException e) {\n    // steer user into CAS password-change flow\n    throw e;\n}","preventionTips":["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"],"tags":["authentication","rest","http-428","password-change"],"backgroundTag":"http-error-response","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}