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

  1. Request a new OTP via POST /accounts/request-otp and enter it promptly.
  2. Ensure the user enters the code before the validity window expires (typically 30 seconds for TOTP).
  3. Check for clock synchronization issues on the user's device.
  4. 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

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

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/8e0b588091bc48e3. Report an issue: GitHub.