bitwarden/server · error · BadRequestException

Invalid token.

Error message

Invalid token.

What it means

BadRequestException with key 'Token' and message 'Invalid token.' is thrown in PUT /authenticator when _userManager.VerifyTwoFactorTokenAsync fails for the Authenticator provider against model.Token. This means the 6-digit TOTP the user typed does not match the time-based code computed from their registered authenticator secret.

Source

Thrown at src/Api/Auth/Controllers/TwoFactorController.cs:149

    public async Task<TwoFactorAuthenticatorUpdateResponseModel> PutAuthenticator(
        [FromBody] TwoFactorAuthenticatorUpdateRequestModel model)
    {
        var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));

        var tokenIsValid =
            _twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken)
            && decryptedToken.Valid
            && decryptedToken.TokenIsValid(user, model.Key);

        if (!tokenIsValid)
        {
            throw new BadRequestException("UserVerificationToken", "User verification failed.");
        }

        if (!await _userManager.VerifyTwoFactorTokenAsync(user,
                CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
        {
            throw new BadRequestException("Token", "Invalid token.");
        }

        await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator);
        return new TwoFactorAuthenticatorUpdateResponseModel(user);
    }

    [HttpPost("authenticator")]
    [Obsolete("This endpoint is deprecated. Use PUT /authenticator instead.")]
    public async Task<TwoFactorAuthenticatorUpdateResponseModel> PostAuthenticator(
        [FromBody] TwoFactorAuthenticatorUpdateRequestModel model)
    {
        return await PutAuthenticator(model);
    }

    [HttpDelete("authenticator")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> DeleteAuthenticator(
        [FromBody] TwoFactorAuthenticatorDeleteRequestModel model)

View on GitHub (pinned to e93b962371)

Solutions

  1. Have the user generate and submit a fresh code from the authenticator app immediately (within the 30s window).
  2. Check for clock drift on the client device or server; TOTP allows a small window but large skew fails.
  3. Re-register the authenticator (get-authenticator flow) if the secret may be out of sync.
  4. Strip whitespace/formatting from the token before submission.

Example fix

// before
api.put('/users/two-factor/authenticator', { userVerificationToken, token: '12 34 56', key })
// after: sanitize and submit current code
const token = code.replace(/\s+/g, '');
api.put('/users/two-factor/authenticator', { userVerificationToken, token, key });
Defensive patterns

Strategy: validation

Validate before calling

const token = String(model.token).replace(/\D/g, '');
if (token.length !== 6) throw new Error('TOTP must be 6 digits');

Type guard

function isValidTotp(t): t is string { return /^\d{6}$/.test(String(t)); }

Try / catch

try { await api.put('/users/two-factor/authenticator', model); }
catch (e) {
  if (e.response?.data?.error?.errors?.Token) { throw new UserFacingError('The code is wrong or expired; generate a new one.'); }
  throw e;
}

Prevention

When it happens

Trigger: PUT /api/users/two-factor/authenticator (TwoFactorController line 149) where model.Token is wrong, expired (outside the TOTP time window), or computed from a different secret than the one registered for the user.

Common situations: Clock skew between the authenticator app and server, the user scanned the wrong QR / registered the secret on a second device, the code was entered after it rotated, or the secret was reset since minting the verification token.

Understand the failure class

Related errors


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