bitwarden/server · error · BadRequestException
User verification failed.
Error message
User verification failed.
What it means
BadRequestException with key 'UserVerificationToken' is thrown in PUT /authenticator (PutAuthenticator) when the user-verification token fails one of: TryUnprotect (tampered/corrupt), decryptedToken.Valid (expired signature), or decryptedToken.TokenIsValid(user, model.Key) (wrong user or key). This is a data-protection token bound to the requesting user and their key, minted by the get-authenticator endpoint.
Source
Thrown at src/Api/Auth/Controllers/TwoFactorController.cs:143
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, data.Key);
var userVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
return new TwoFactorAuthenticatorResponseModel(data, userVerificationToken);
}
[HttpPut("authenticator")]
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);View on GitHub (pinned to e93b962371)
Solutions
- Re-call POST /two-factor/get-authenticator (with secret verification) to mint a fresh UserVerificationToken, then submit it immediately.
- Ensure the same authenticated user and master key are used for both minting and the PUT.
- Do not cache or reuse the token across logins; treat it as single-use and short-lived.
- Confirm the token string is not truncated/altered in transit (encoding).
Example fix
// before: reusing an old/cached token
api.put('/users/two-factor/authenticator', { userVerificationToken: oldToken, ... })
// after: mint fresh each flow
const { userVerificationToken } = await api.post('/users/two-factor/get-authenticator', { masterPasswordHash });
api.put('/users/two-factor/authenticator', { userVerificationToken, token, key }); Defensive patterns
Strategy: validation
Validate before calling
if (!model.userVerificationToken) { const r = await api.post('/users/two-factor/get-authenticator', { masterPasswordHash }); model.userVerificationToken = r.userVerificationToken; } Type guard
function hasFreshVerificationToken(m, mintedAt): m is AuthenticatorUpdateModel & { userVerificationToken: string } {
return typeof m.userVerificationToken === 'string' && Date.now() - mintedAt < 5 * 60 * 1000;
} Try / catch
try { await api.put('/users/two-factor/authenticator', model); }
catch (e) {
if (e.response?.status === 400 && e.response.data?.error?.errors?.UserVerificationToken) {
model.userVerificationToken = (await api.post('/users/two-factor/get-authenticator', { masterPasswordHash })).userVerificationToken;
return api.put('/users/two-factor/authenticator', model);
}
throw e;
} Prevention
- Mint a fresh UserVerificationToken immediately before each mutating two-factor call.
- Never reuse tokens across users, sessions, or providers.
- Treat verification tokens as short-lived and single-use.
When it happens
Trigger: PUT /api/users/two-factor/authenticator (TwoFactorController line 143) submitted with a UserVerificationToken that is missing, expired, decrypted-but-invalid, or bound to a different user/key than the current principal.
Common situations: The client reused a token from a different session/user, the token expired before submission, the user changed their master key between minting and using the token, or the get-authenticator step (which issues the token) was skipped.
Related errors
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/23dcc21395b07c05.
Report an issue: GitHub.