apereo/cas · warning · FailedLoginException

Authorization of OTP token

Error message

Authorization of OTP token [{}] has failed

What it means

GoogleAuthenticatorDeleteAccountAction validates the OTP token supplied when removing a device. If validator.validate returns null the removal request is rejected: this warning is logged and FailedLoginException is thrown, so the account is not deleted.

Solutions

  1. Ask the user to submit a fresh code from the authenticator entry corresponding to the device being deleted
  2. Verify the accountId/registrationId selection matches the account whose token is submitted
  3. Check clock sync and validation window settings if all valid codes are rejected
  4. If the account secret in the repository is corrupted, re-register the device before removal
Defensive patterns

Strategy: retry

Validate before calling

boolean codeLooksValid = code != null && code.matches("\\d{6}");
// and confirm the selected device id matches the code source

Try / catch

try {
    event = deleteAccountAction.execute(requestContext);
} catch (FailedLoginException e) {
    // prompt for a fresh code bound to the device being removed
}

Prevention

When it happens

Trigger: doExecuteInternal receives a GoogleAuthenticatorTokenCredential for account removal whose token fails validation (wrong code, expired window, reused token, or token not bound to the account being deleted).

Common situations: User confirms device removal with a stale or mistyped code; token already consumed; wrong registrationId selected so codes are validated against the wrong account secret.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/aa24a4819e56b852. 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:57

    protected @Nullable Event doExecuteInternal(final RequestContext requestContext) throws Throwable {
        val requestParameters = requestContext.getRequestParameters();
        val accountId = requestParameters.getRequired(OneTimeTokenAccountConfirmSelectionRegistrationAction.REQUEST_PARAMETER_ACCOUNT_ID, Long.class);
        val validate = requestParameters.getBoolean(OneTimeTokenAccountSaveRegistrationAction.REQUEST_PARAMETER_VALIDATE);
        val account = repository.get(accountId);

        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);
    }

View on GitHub (pinned to e7288fc434)