apereo/cas · error · FailedLoginException
Authorization of OTP token
Error message
Authorization of OTP token [{}] has failed for [{}] What it means
In GoogleAuthenticatorValidateTokenAction the OTP token passed validation but fails the authorization check that ties the validated token to the authenticated principal's registered GAuth account. CAS treats this as a failed login: a FailedLoginException is thrown with the message "Failed to authenticate code <token>". This indicates the token was syntactically valid but did not correspond to an account owned by the given principal.
Solutions
- Confirm the OTP was generated from the device registered to the authenticated principal
- Check the GAuth account registry (JSON/JDBC/Mongo) for the principal's account and verify its secret and user id fields are correct
- Re-register the device for the user if the stored secret does not match the authenticator app
- Review any custom OneTimeTokenValidator/account-lookup beans for principal-mapping errors
Example fix
// before (account bound to wrong user)
{"id": 1, "username": "otheruser", "secret": "..."}
// after
{"id": 1, "username": "correctuser", "secret": "..."} Defensive patterns
Strategy: try-catch
Validate before calling
if (validatedToken == null || !principal.getId().equals(validatedToken.getPrincipalId())) {
throw new FailedLoginException("OTP token not owned by current principal");
} Try / catch
try {
authCtx = gauthValidateAction.execute(ctx);
} catch (FailedLoginException e) {
LOGGER.warn("GAuth authorization failed: {}", e.getMessage());
return flowError(e);
} Prevention
- Re-register devices whose stored secrets drift from the authenticator app
- Verify GAuth account registry records bind the correct username to each registration
- Avoid sharing/replaying OTPs across sessions or accounts
When it happens
Trigger: A one-time token is presented whose computed value matches but whose owning account is not linked to the current authentication's principal (wrong account binding, principal mismatch, or account registry lookup returning a registration for another user).
Common situations: Users switching accounts mid-session; tokens generated against a different registration; missing or corrupted GAuth account records in the registry after import/restore; custom token validators that skip account-ownership checks.
Related errors
- Failed to authenticate code
- cannot be found in the registry
- cannot reuse OTP
- Failed to authenticate code
- Failed to authenticate code
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/7316de92d7912138.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/web/flow/GoogleAuthenticatorValidateTokenAction.java:52
@Override
protected @Nullable Event doExecuteInternal(final RequestContext requestContext) throws Throwable {
val token = requestContext.getRequestParameters().getRequired(GoogleAuthenticatorSaveRegistrationAction.REQUEST_PARAMETER_TOKEN, String.class);
val accountId = requestContext.getRequestParameters().getRequired(OneTimeTokenAccountConfirmSelectionRegistrationAction.REQUEST_PARAMETER_ACCOUNT_ID, Long.class);
val authentication = WebUtils.getAuthentication(requestContext);
val tokenCredential = new GoogleAuthenticatorTokenCredential(token, accountId);
val validatedToken = validator.validate(authentication, tokenCredential);
if (validatedToken != null) {
val principal = authentication.getPrincipal().getId();
LOGGER.debug("Validated OTP token [{}] successfully for [{}]", validatedToken, principal);
val validate = requestContext.getRequestParameters().getBoolean(OneTimeTokenAccountSaveRegistrationAction.REQUEST_PARAMETER_VALIDATE);
if (validate == null || !validate) {
validator.store(validatedToken);
}
return success();
}
LOGGER.warn("Authorization of OTP token [{}] has failed for [{}]", token, authentication.getPrincipal().getId());
throw new FailedLoginException("Failed to authenticate code " + token);
}
}
View on GitHub (pinned to e7288fc434)