apereo/cas · warning · FailedLoginException

Authorization of OTP token

Error message

Authorization of OTP token [{}] has failed

What it means

During account registration confirmation, GoogleAuthenticatorConfirmAccountRegistrationAction validates the submitted OTP token with the newly registered account. If validation fails (null result) it logs this warning and throws FailedLoginException, aborting the registration-confirmation webflow step.

Solutions

  1. Have the user retry with a freshly generated code immediately after scanning the QR code
  2. Synchronize clocks (NTP) on the CAS server and the user's device; widen the validation window if needed
  3. Ensure the account being confirmed matches the one whose QR code was scanned (registrationId/accountId)
  4. Clear partially registered state and restart the registration flow if the stored secret is stale
Defensive patterns

Strategy: retry

Validate before calling

// client-side sanity check before submitting
boolean codeLooksValid = code != null && code.matches("\\d{6}");

Try / catch

try {
    event = confirmRegistrationAction.execute(requestContext);
} catch (FailedLoginException e) {
    // prompt user to retry with a freshly generated code
}

Prevention

When it happens

Trigger: doExecuteInternal runs with a GoogleAuthenticatorTokenCredential whose token validator.validate(authentication, tokenCredential) returns null — code wrong, expired (outside window), reused, or not for the registered account.

Common situations: User scans the QR code with the wrong app account so codes don't match; clock skew between server and phone; user waits too long before submitting the code; token already consumed by a prior attempt.

Related errors


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

Appendix: source

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

    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);
        Objects.requireNonNull(account, "Account cannot be null");
        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);
                accountRegistrationVerified(requestContext, account);
                return success();
            }
            LOGGER.warn("Authorization of OTP token [{}] has failed", token);
            throw new FailedLoginException("Failed to authenticate code " + token);
        }

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

        accountRegistrationUnverified(requestContext, account);
        return success();
    }

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

    protected void accountRegistrationUnverified(final RequestContext requestContext, final OneTimeTokenAccount account) {

View on GitHub (pinned to e7288fc434)