bitwarden/server · error · BadRequestException
Invalid token
Error message
Invalid token
What it means
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'.
Source
Thrown at src/Api/Auth/Controllers/AccountsController.cs:812
}
[HttpPost("request-otp")]
public async Task PostRequestOTP()
{
var user = await _userService.GetUserByPrincipalAsync(User);
await _userService.SendOTPAsync(user);
}
[HttpPost("verify-otp")]
public async Task VerifyOTP([FromBody] VerifyOTPRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (!await _userService.VerifyOTPAsync(user, model.OTP))
{
await Task.Delay(2000);
throw new BadRequestException("Token", "Invalid token");
}
}
[AllowAnonymous]
[HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
{
var user = await _userRepository.GetByEmailAsync(request.Email);
if (user == null || !await _userService.VerifySecretAsync(user, request.Secret))
{
// If the user is not found, or the secret is not valid, we still return
// a success response, to avoid account enumeration via response shape.
return;
}
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
}
[HttpPut("verify-devices")]View on GitHub (pinned to e93b962371)
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).
Example fix
// before: submitting a stale or incorrect OTP
var resp = await client.PostAsJsonAsync("/accounts/verify-otp",
new VerifyOTPRequestModel { OTP = staleCode }); // 400 Invalid token
// after: request fresh OTP and submit immediately
await client.PostAsync("/accounts/request-otp", null);
// user reads new code from email/authenticator
var resp = await client.PostAsJsonAsync("/accounts/verify-otp",
new VerifyOTPRequestModel { OTP = freshCode }); Defensive patterns
Strategy: retry
Validate before calling
// Check OTP freshness before submitting
if (otpGeneratedAt < DateTime.UtcNow.AddSeconds(-30)) {
// OTP likely expired — request a new one
await client.PostAsync("/accounts/request-otp", null);
return Error("OTP may have expired. A new one has been sent.");
}
// Validate format
if (string.IsNullOrWhiteSpace(model.OTP) || model.OTP.Length < expectedOtpLength) {
return Error("OTP is missing or too short");
} Try / catch
try {
var resp = await client.PostAsJsonAsync("/accounts/verify-otp", model);
resp.EnsureSuccessStatusCode();
} catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) {
// OTP was wrong or expired — request a new one
await client.PostAsync("/accounts/request-otp", null);
ShowUserError("The OTP was invalid or expired. A new one has been sent to your email.");
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- User verification failed.
- You cannot remove yourself.
- Organization already belongs to a provider.
- The organization is subscribed to Secrets Manager. Please co
- Provider must be of type Reseller in order to assign Organiz
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/8e0b588091bc48e3.
Report an issue: GitHub.