{"record":{"id":"8e0b588091bc48e3","repo":"bitwarden/server","slug":"invalid-token-8e0b58","errorCode":null,"errorMessage":"Invalid token","messagePattern":"Invalid token","errorType":"validation","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Auth/Controllers/AccountsController.cs","lineNumber":812,"sourceCode":"    }\n\n    [HttpPost(\"request-otp\")]\n    public async Task PostRequestOTP()\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        await _userService.SendOTPAsync(user);\n    }\n\n    [HttpPost(\"verify-otp\")]\n    public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n\n        if (!await _userService.VerifyOTPAsync(user, model.OTP))\n        {\n            await Task.Delay(2000);\n            throw new BadRequestException(\"Token\", \"Invalid token\");\n        }\n    }\n\n    [AllowAnonymous]\n    [HttpPost(\"resend-new-device-otp\")]\n    public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)\n    {\n        var user = await _userRepository.GetByEmailAsync(request.Email);\n        if (user == null || !await _userService.VerifySecretAsync(user, request.Secret))\n        {\n            // If the user is not found, or the secret is not valid, we still return\n            // a success response, to avoid account enumeration via response shape.\n            return;\n        }\n        await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);\n    }\n\n    [HttpPut(\"verify-devices\")]","sourceCodeStart":794,"sourceCodeEnd":830,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/AccountsController.cs#L794-L830","documentation":"Thrown as BadRequestException(\"Token\", \"Invalid token\") (HTTP 400) from POST /accounts/verify-otp. _userService.VerifyOTPAsync(user, model.OTP) returns false — the one-time password does not match the expected value. A 2-second delay precedes the throw to slow brute-force attempts. The error key is 'Token' and the message is 'Invalid token'.","triggerScenarios":"POST /accounts/verify-otp is called with an OTP that has expired, was already consumed, or is simply incorrect. The OTP system may use time-based or counter-based codes that must match within a validity window.","commonSituations":"OTP expired between the user reading it and submitting (common with 30-second TOTP windows). User typed the wrong code. The OTP was already used in a previous request (replay protection). Clock skew between the OTP-generating device and the server. User has multiple OTP sources and used the wrong one.","solutions":["Request a new OTP via POST /accounts/request-otp and enter it promptly.","Ensure the user enters the code before the validity window expires (typically 30 seconds for TOTP).","Check for clock synchronization issues on the user's device.","Verify the user is using the correct OTP delivery method (email vs authenticator app)."],"exampleFix":"// before: submitting a stale or incorrect OTP\nvar resp = await client.PostAsJsonAsync(\"/accounts/verify-otp\",\n    new VerifyOTPRequestModel { OTP = staleCode }); // 400 Invalid token\n\n// after: request fresh OTP and submit immediately\nawait client.PostAsync(\"/accounts/request-otp\", null);\n// user reads new code from email/authenticator\nvar resp = await client.PostAsJsonAsync(\"/accounts/verify-otp\",\n    new VerifyOTPRequestModel { OTP = freshCode });","handlingStrategy":"retry","validationCode":"// Check OTP freshness before submitting\nif (otpGeneratedAt < DateTime.UtcNow.AddSeconds(-30)) {\n    // OTP likely expired — request a new one\n    await client.PostAsync(\"/accounts/request-otp\", null);\n    return Error(\"OTP may have expired. A new one has been sent.\");\n}\n\n// Validate format\nif (string.IsNullOrWhiteSpace(model.OTP) || model.OTP.Length < expectedOtpLength) {\n    return Error(\"OTP is missing or too short\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    var resp = await client.PostAsJsonAsync(\"/accounts/verify-otp\", model);\n    resp.EnsureSuccessStatusCode();\n} catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) {\n    // OTP was wrong or expired — request a new one\n    await client.PostAsync(\"/accounts/request-otp\", null);\n    ShowUserError(\"The OTP was invalid or expired. A new one has been sent to your email.\");\n}","preventionTips":["Submit the OTP within its validity window (typically 30 seconds for TOTP).","Do not retry with the same OTP after a failure — request a new one.","Synchronize the user's device clock with NTP to avoid TOTP drift."],"tags":["otp","verification","bad-request","brute-force-protection","totp"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}