{"record":{"id":"c5b86a3b81cc6e8b","repo":"apereo/cas","slug":"cannot-reuse-otp","errorCode":null,"errorMessage":" cannot reuse OTP ","messagePattern":" cannot reuse OTP ","errorType":"exception","errorClass":"AccountExpiredException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/credential/GoogleAuthenticatorOneTimeTokenCredentialValidator.java","lineNumber":65,"sourceCode":"            throw new PreventedException(\"Invalid non-numeric OTP format specified.\");\n        }\n\n        val uid = authentication.getPrincipal().getId();\n        val otp = Integer.parseInt(tokenCredential.getToken());\n        LOGGER.trace(\"Received OTP [{}] assigned to account [{}]\", otp, tokenCredential.getAccountId());\n\n        LOGGER.trace(\"Received principal id [{}]. Attempting to locate account in credential repository...\", uid);\n        val accounts = credentialRepository.get(uid);\n        if (accounts == null || accounts.isEmpty()) {\n            throw new AccountNotFoundException(uid + \" cannot be found in the registry\");\n        }\n\n        if (accounts.size() > 1 && tokenCredential.getAccountId() == null) {\n            throw new PreventedException(\"Account identifier must be specified if multiple accounts are registered for \" + uid);\n        }\n        LOGGER.trace(\"Attempting to locate OTP token [{}] in token repository for [{}]...\", otp, uid);\n        if (tokenRepository.exists(uid, otp)) {\n            throw new AccountExpiredException(uid + \" cannot reuse OTP \" + otp + \" as it may be expired/invalid\");\n        }\n\n        LOGGER.debug(\"Attempting to authorize OTP token [{}]...\", otp);\n        val result = getAuthorizedAccountForToken(tokenCredential, accounts)\n            .or(() -> getAuthorizedScratchCodeForToken(tokenCredential, authentication, accounts));\n        return result\n            .map(acct -> new GoogleAuthenticatorToken(otp, uid))\n            .orElse(null);\n    }\n\n    @Override\n    @CanIgnoreReturnValue\n    public OneTimeTokenCredentialValidator<GoogleAuthenticatorTokenCredential, GoogleAuthenticatorToken> store(\n        final GoogleAuthenticatorToken validatedToken) {\n        if (tokenRepository.store(validatedToken) == null) {\n            throw new IllegalArgumentException(validatedToken.getUserId() + \" cannot reuse OTP \"\n                + validatedToken.getToken() + \" as it may be expired/invalid\");\n        }","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-gauth-core/src/main/java/org/apereo/cas/gauth/credential/GoogleAuthenticatorOneTimeTokenCredentialValidator.java#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// check before submit\nif (tokenRepository.exists(uid, otp)) return \"This code was already used; wait for a new one\";","typeGuard":null,"tryCatchPattern":"try {\n    validator.validate(tokenCredential, authentication);\n} catch (AccountExpiredException e) {\n    return failure(\"Code already used — generate a fresh OTP\");\n}","preventionTips":["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"],"tags":["gauth","otp","replay","mfa"],"backgroundTag":"otp-reuse-rejected","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}