apereo/cas · error · FailedLoginException

Unauthorized account removal attempt

Error message

Unauthorized account removal attempt 

What it means

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.

Solutions

  1. Follow the flow: submit a valid OTP on the account-removal verification screen before deletion
  2. Fix flow definitions so the delete state cannot be reached without prior verification or a bound token
  3. Clear stale webflow state (new flow execution/session) if the verified flag was lost
  4. If integrating programmatically, always call the verification path (token submitted) before invoking delete
Defensive patterns

Strategy: validation

Validate before calling

// guard the action call
if (token == null && !isAccountRemovalVerified(requestContext, account))
    throw new IllegalStateException("Account removal requires prior OTP verification");

Try / catch

try {
    action.executeInternal(requestContext);
} catch (FailedLoginException e) {
    return restartRemovalFlow();
}

Prevention

When it happens

Trigger: 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>'.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/web/flow/GoogleAuthenticatorDeleteAccountAction.java:63

        if (BooleanUtils.isTrue(validate)) {
            val token = requestParameters.getRequired(GoogleAuthenticatorSaveRegistrationAction.REQUEST_PARAMETER_TOKEN, String.class);
            val authentication = WebUtils.getAuthentication(requestContext);
            val principal = authentication.getPrincipal().getId();
            LOGGER.debug("Validating account [{}] with token [{}] for principal [{}]", accountId, token, principal);
            val tokenCredential = new GoogleAuthenticatorTokenCredential(token, accountId);
            val validatedToken = validator.validate(authentication, tokenCredential);
            if (validatedToken != null) {
                LOGGER.debug("Validated OTP token [{}] successfully for [{}]", validatedToken, principal);
                accountRemovalVerified(requestContext, account);
                return success();
            }
            LOGGER.warn("Authorization of OTP token [{}] has failed", token);
            throw new FailedLoginException("Failed to authenticate code " + token);
        }

        if (!isAccountRemovalVerified(requestContext, account)) {
            LOGGER.warn("Account removal is not verified for [{}]", account.getId());
            throw new FailedLoginException("Unauthorized account removal attempt " + account.getId());
        }

        LOGGER.debug("Deleting account [{}]", account.getId());
        repository.delete(account.getId());
        return success();
    }

    protected void accountRemovalVerified(final RequestContext requestContext, final OneTimeTokenAccount account) {
        account.getProperties().add(ACCOUNT_PROPERTY_REMOVAL_VERIFIED);
        repository.update(account);
    }

    protected boolean isAccountRemovalVerified(final RequestContext requestContext, final OneTimeTokenAccount account) {
        return account.getProperties().contains(ACCOUNT_PROPERTY_REMOVAL_VERIFIED);
    }
}

View on GitHub (pinned to e7288fc434)