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

  1. Generate a fresh OTP from the authenticator app and retry — the old one is permanently consumed
  2. Fix duplicate form submissions on the client (disable resubmission, one-time request tokens)
  3. If running multiple CAS nodes, ensure tokenRepository storage is shared consistently so consumed tokens are recorded everywhere
  4. 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

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


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)