{"record":{"id":"794da8016e2ac049","repo":"apereo/cas","slug":"password-reset-token-could-not-be-verified-to-dete","errorCode":null,"errorMessage":"Password reset token could not be verified to determine username","messagePattern":"Password reset token could not be verified to determine username","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-pm-webflow/src/main/java/org/apereo/cas/pm/web/flow/actions/ValidatePasswordResetTokenAction.java","lineNumber":42,"sourceCode":" */\n@RequiredArgsConstructor\n@Slf4j\npublic class ValidatePasswordResetTokenAction extends BaseCasWebflowAction {\n    private final PasswordManagementService passwordManagementService;\n\n    private final TicketRegistry ticketRegistry;\n\n    @Override\n    protected @Nullable Event doExecuteInternal(final RequestContext requestContext) {\n        try {\n            val transientTicket = requestContext.getRequestParameters()\n                .get(PasswordManagementService.PARAMETER_PASSWORD_RESET_TOKEN);\n            if (StringUtils.isNotBlank(transientTicket)) {\n                val tst = ticketRegistry.getTicket(transientTicket, TransientSessionTicket.class);\n                val token = tst.getProperties().get(PasswordManagementService.PARAMETER_TOKEN).toString();\n                val username = passwordManagementService.parseToken(token);\n                if (StringUtils.isBlank(username)) {\n                    throw new IllegalArgumentException(\"Password reset token could not be verified to determine username\");\n                }\n                return eventFactory.event(this, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD);\n            }\n            val doChange = requestContext.getRequestParameters()\n                .get(PasswordManagementService.PARAMETER_DO_CHANGE_PASSWORD);\n            if (StringUtils.isNotBlank(doChange) && BooleanUtils.toBoolean(doChange)) {\n                return eventFactory.event(this, CasWebflowConstants.TRANSITION_ID_RESET_PASSWORD);\n            }\n\n            return null;\n        } catch (final Exception e) {\n            LoggingUtils.warn(LOGGER, e);\n            return eventFactory.event(this, CasWebflowConstants.TRANSITION_ID_INVALID_PASSWORD_RESET_TOKEN);\n        }\n    }\n}\n","sourceCodeStart":24,"sourceCodeEnd":59,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-pm-webflow/src/main/java/org/apereo/cas/pm/web/flow/actions/ValidatePasswordResetTokenAction.java#L24-L59","documentation":"IllegalArgumentException thrown by ValidatePasswordResetTokenAction when the password-reset token extracted from the transient ticket cannot be parsed by passwordManagementService.parseToken(token) into a non-blank username. CAS uses this to confirm which account the reset token belongs to before allowing the password change.","triggerScenarios":"A user opens the password-reset flow with a PARAMETER_PASSWORD_RESET_TOKEN whose underlying JWT/token resolves to a blank username: expired token, token signed with a different secret than the one used to create it, malformed token, or a token produced by a different CAS node/ configuration.","commonSituations":"Reset link clicked after token expiration; cas.authn.pm reset token crypto signing key changed or differs across clustered nodes; link truncated in email; user manually edits the token parameter.","solutions":["Request a new password-reset email and use the fresh link promptly.","Ensure cas.authn.pm.reset.crypto.encryption/signing keys are identical on every node in the cluster and have not been rotated since the token was issued.","Verify the reset link URL was not truncated or re-encoded by the mail client.","Enable debug logging on PasswordManagementService to see why parseToken returned blank (signature/expiry)."],"exampleFix":"// before (mismatched keys across nodes)\ncas.authn.pm.reset.crypto.signing.key=different-per-node\n// after\n# same shared key in every node's properties\ncas.authn.pm.reset.crypto.signing.key=[shared-secret]","handlingStrategy":"validation","validationCode":"// Parse the token yourself before driving the flow\nString username = pmService.parseToken(resetToken);\nif (StringUtils.isBlank(username)) { return error(\"reset link is invalid or expired\"); }","typeGuard":null,"tryCatchPattern":"try { return validateAction.execute(ctx); } catch (IllegalArgumentException e) { WebUtils.addErrorMessageToContext(ctx, \"screen.pm.reset.invalid\"); return errorEvent; }","preventionTips":["Share identical PM reset-token crypto keys across all cluster nodes.","Click reset links promptly; treat them as short-lived.","Avoid URL rewriters that truncate or re-encode the token query parameter."],"tags":["password-reset","token-validation","jwt"],"backgroundTag":"invalid-argument-value","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"}