{"record":{"id":"587a7428ffbde20e","repo":"apereo/cas","slug":"unauthorized-account-removal-attempt","errorCode":null,"errorMessage":"Unauthorized account removal attempt ","messagePattern":"Unauthorized account removal attempt ","errorType":"exception","errorClass":"FailedLoginException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/web/flow/GoogleAuthenticatorDeleteAccountAction.java","lineNumber":63,"sourceCode":"        if (BooleanUtils.isTrue(validate)) {\n            val token = requestParameters.getRequired(GoogleAuthenticatorSaveRegistrationAction.REQUEST_PARAMETER_TOKEN, String.class);\n            val authentication = WebUtils.getAuthentication(requestContext);\n            val principal = authentication.getPrincipal().getId();\n            LOGGER.debug(\"Validating account [{}] with token [{}] for principal [{}]\", accountId, token, principal);\n            val tokenCredential = new GoogleAuthenticatorTokenCredential(token, accountId);\n            val validatedToken = validator.validate(authentication, tokenCredential);\n            if (validatedToken != null) {\n                LOGGER.debug(\"Validated OTP token [{}] successfully for [{}]\", validatedToken, principal);\n                accountRemovalVerified(requestContext, account);\n                return success();\n            }\n            LOGGER.warn(\"Authorization of OTP token [{}] has failed\", token);\n            throw new FailedLoginException(\"Failed to authenticate code \" + token);\n        }\n\n        if (!isAccountRemovalVerified(requestContext, account)) {\n            LOGGER.warn(\"Account removal is not verified for [{}]\", account.getId());\n            throw new FailedLoginException(\"Unauthorized account removal attempt \" + account.getId());\n        }\n\n        LOGGER.debug(\"Deleting account [{}]\", account.getId());\n        repository.delete(account.getId());\n        return success();\n    }\n\n    protected void accountRemovalVerified(final RequestContext requestContext, final OneTimeTokenAccount account) {\n        account.getProperties().add(ACCOUNT_PROPERTY_REMOVAL_VERIFIED);\n        repository.update(account);\n    }\n\n    protected boolean isAccountRemovalVerified(final RequestContext requestContext, final OneTimeTokenAccount account) {\n        return account.getProperties().contains(ACCOUNT_PROPERTY_REMOVAL_VERIFIED);\n    }\n}\n","sourceCodeStart":45,"sourceCodeEnd":80,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/web/flow/GoogleAuthenticatorDeleteAccountAction.java#L45-L80","documentation":"GoogleAuthenticatorDeleteAccountAction throws this when no token was provided and the account removal has not been previously verified in flow scope (isAccountRemovalVerified is false). It blocks unverified account deletion — removal must be confirmed either by a prior OTP verification step or by submitting a valid token now.","triggerScenarios":"doExecuteInternal is invoked with null/empty token and flow scope lacks the removal-verified flag — e.g., the delete-account state is reached out of order or the verification step was skipped — throwing FailedLoginException 'Unauthorized account removal attempt <accountId>'.","commonSituations":"Navigating directly to the delete-account state via back/forward or deep link; customized webflow bypassing the verify state; lost webflow/session state dropping the verified marker; automation calling the action without a token.","solutions":["Follow the flow: submit a valid OTP on the account-removal verification screen before deletion","Fix flow definitions so the delete state cannot be reached without prior verification or a bound token","Clear stale webflow state (new flow execution/session) if the verified flag was lost","If integrating programmatically, always call the verification path (token submitted) before invoking delete"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// guard the action call\nif (token == null && !isAccountRemovalVerified(requestContext, account))\n    throw new IllegalStateException(\"Account removal requires prior OTP verification\");","typeGuard":null,"tryCatchPattern":"try {\n    action.executeInternal(requestContext);\n} catch (FailedLoginException e) {\n    return restartRemovalFlow();\n}","preventionTips":["Do not deep-link to the delete-account state; always pass through verification","Bind the token parameter in the webflow definition","Preserve webflow session state between steps","In custom integrations, always submit a token when calling this action"],"tags":["gauth","webflow","mfa","authorization"],"backgroundTag":"invalid-state-transition","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"}