apereo/cas · error · AccountExpiredException
cannot reuse OTP
Error message
cannot reuse OTP
What it means
GoogleAuthenticatorOneTimeTokenCredentialValidator rejects an OTP that was already used: before authorizing the token it checks tokenRepository.exists(uid, otp) and throws AccountExpiredException '<uid> cannot reuse OTP <otp> as it may be expired/invalid'. CAS stores every consumed token to enforce one-time-use semantics, preventing replay attacks.
Solutions
- Generate a fresh OTP from the authenticator app and retry — the old one is permanently consumed
- Fix duplicate form submissions on the client (disable resubmission, one-time request tokens)
- If running multiple CAS nodes, ensure tokenRepository storage is shared consistently so consumed tokens are recorded everywhere
- Check for scripts/integrations retrying the same credential automatically
Defensive patterns
Strategy: try-catch
Validate before calling
// check before submit if (tokenRepository.exists(uid, otp)) return "This code was already used; wait for a new one";
Try / catch
try {
validator.validate(tokenCredential, authentication);
} catch (AccountExpiredException e) {
return failure("Code already used — generate a fresh OTP");
} Prevention
- Disable form resubmission; use PRG pattern on the login page
- Share the token repository across clustered nodes
- Never retry automatically with the same OTP
- Educate users: each code is single-use
When it happens
Trigger: validator.validate() finds tokenRepository.exists(uid, otp) returns true — the same uid+otp combination was already stored (from a previous successful validation where validator.store(validatedToken) was called) and is being submitted again.
Common situations: User double-submits a login form (refresh/back button resending the same OTP); client retries a timed-out request with the same code; multiple browser tabs logged in with the same authenticator entry; replay within the 30s window of the same TOTP.
Related errors
- Failed to authenticate code
- cannot be found in the registry
- Failed to authenticate code
- Failed to authenticate code
- Failed to authenticate code
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/c5b86a3b81cc6e8b.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/credential/GoogleAuthenticatorOneTimeTokenCredentialValidator.java:65
throw new PreventedException("Invalid non-numeric OTP format specified.");
}
val uid = authentication.getPrincipal().getId();
val otp = Integer.parseInt(tokenCredential.getToken());
LOGGER.trace("Received OTP [{}] assigned to account [{}]", otp, tokenCredential.getAccountId());
LOGGER.trace("Received principal id [{}]. Attempting to locate account in credential repository...", uid);
val accounts = credentialRepository.get(uid);
if (accounts == null || accounts.isEmpty()) {
throw new AccountNotFoundException(uid + " cannot be found in the registry");
}
if (accounts.size() > 1 && tokenCredential.getAccountId() == null) {
throw new PreventedException("Account identifier must be specified if multiple accounts are registered for " + uid);
}
LOGGER.trace("Attempting to locate OTP token [{}] in token repository for [{}]...", otp, uid);
if (tokenRepository.exists(uid, otp)) {
throw new AccountExpiredException(uid + " cannot reuse OTP " + otp + " as it may be expired/invalid");
}
LOGGER.debug("Attempting to authorize OTP token [{}]...", otp);
val result = getAuthorizedAccountForToken(tokenCredential, accounts)
.or(() -> getAuthorizedScratchCodeForToken(tokenCredential, authentication, accounts));
return result
.map(acct -> new GoogleAuthenticatorToken(otp, uid))
.orElse(null);
}
@Override
@CanIgnoreReturnValue
public OneTimeTokenCredentialValidator<GoogleAuthenticatorTokenCredential, GoogleAuthenticatorToken> store(
final GoogleAuthenticatorToken validatedToken) {
if (tokenRepository.store(validatedToken) == null) {
throw new IllegalArgumentException(validatedToken.getUserId() + " cannot reuse OTP "
+ validatedToken.getToken() + " as it may be expired/invalid");
}View on GitHub (pinned to e7288fc434)