apereo/cas · error · FailedLoginException

Failed to authenticate code

Error message

Failed to authenticate code 

What it means

GoogleAuthenticatorAuthenticationHandler could not validate the submitted OTP token. GoogleAuthenticatorOneTimeTokenCredentialValidator returned null (token invalid, already used, wrong secret, or account mismatch) so the handler logs a warning and throws FailedLoginException. It means the user-entered TOTP/scratch code did not authorize against any registered GAuth account.

Solutions

  1. Have the user generate a fresh code from the authenticator app and retry — reused codes are rejected by tokenRepository.exists(uid, otp)
  2. Verify time sync (NTP) on the CAS server; large clock drift invalidates TOTP codes
  3. Check the registered account record's secretKey matches the QR code the user scanned
  4. If multiple accounts exist for the user, supply the correct accountId on the credential
Defensive patterns

Strategy: try-catch

Validate before calling

// client-side sanity before submit
if (!/^\d{6}$/.test(otp) && !/^\d{8}$/.test(otp)) reject("OTP must be 6 or 8 digits");

Try / catch

try {
    result = handler.authenticate(tokenCredential);
} catch (FailedLoginException e) {
    return failure("Invalid or expired code — generate a new one and retry");
}

Prevention

When it happens

Trigger: doAuthentication calls validator.validate(tokenCredential, authentication); when the returned validatedToken is null (no authorized account matched, token reused, account not found, clock drift past window) the handler throws FailedLoginException("Failed to authenticate code " + credential).

Common situations: User types an expired or already-consumed code; phone/server clock drift exceeding the allowed window; wrong secret registered for the account; user entering a TOTP from a different account when multiple accounts exist without specifying accountId.

Understand the failure class

Related errors


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

Appendix: source

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

    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)