apereo/cas · warning · FailedLoginException

Authorization of OTP token

Error message

Authorization of OTP token [{}] has failed

What it means

The GoogleAuthenticatorAuthenticationHandler authenticated the OTP token format but the token was not valid for the user's registered account, so the handler logs a warning and throws FailedLoginException. This is the normal rejection path for an incorrect, expired, or already-used one-time code during multifactor login.

Solutions

  1. Have the user enter a freshly generated code from their authenticator app
  2. Check clock synchronization (NTP) on both the server and the user's device, and review cas.authn.mfa.gauth.core.* window settings
  3. Verify the account record exists in the token repository for the authenticating principal (scratch codes/secret correct)
  4. Clear any previously stored tokens if token reuse prevention is rejecting valid codes

Example fix

// before (server rejects all codes due to clock drift)
// cas.authn.mfa.gauth.core.time-step-size=30, window unchanged
// after: widen validation window in cas.properties
cas.authn.mfa.gauth.core.window-size=5
Defensive patterns

Strategy: validation

Validate before calling

// before relying on login succeeding, ensure an account exists for the user
Collection<? extends OneTimeTokenAccount> accts = repository.get(username);
boolean canAttempt = (accts != null && !accts.isEmpty());

Try / catch

try {
    handlerResult = handler.doAuthentication(credential, service, appContext);
} catch (FailedLoginException e) {
    // treat as invalid OTP: prompt user for a fresh code, do not lock immediately
}

Prevention

When it happens

Trigger: doAuthentication calls validator.validate() which returns null (token does not match any window/scratch code for the user's stored secret, was already used, or no account exists); the handler then logs 'Authorization of OTP token has failed' and throws.

Common situations: User mistypes or mistranscribes the 6-digit code from their authenticator app; device clock drift pushes the code outside the validation window; the same token was already consumed (one-time use enforcement); the user's account secret was re-registered but the client still shows old codes.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/GoogleAuthenticatorAuthenticationHandler.java:71

    @Override
    public boolean supports(final Credential credential) {
        return GoogleAuthenticatorTokenCredential.class.isAssignableFrom(credential.getClass());
    }

    @Override
    protected AuthenticationHandlerExecutionResult doAuthentication(final Credential credential, final Service service) throws Throwable {
        val tokenCredential = (GoogleAuthenticatorTokenCredential) credential;
        val authentication = Objects.requireNonNull(WebUtils.getInProgressAuthentication());
        Objects.requireNonNull(authentication, "No authentication is available to determine the principal");
        val validatedToken = validator.validate(authentication, tokenCredential);
        if (validatedToken != null) {
            val principal = authentication.getPrincipal().getId();
            LOGGER.debug("Validated OTP token [{}] successfully for [{}]", validatedToken, principal);
            validator.store(validatedToken);
            LOGGER.debug("Creating authentication result and building principal for [{}]", principal);
            return createHandlerResult(tokenCredential, principalFactory.createPrincipal(principal));
        }
        LOGGER.warn("Authorization of OTP token [{}] has failed", credential);
        throw new FailedLoginException("Failed to authenticate code " + credential);
    }
}

View on GitHub (pinned to e7288fc434)