bitwarden/server · error · BadRequestException
User verification failed.
Error message
User verification failed.
What it means
Thrown as a 400 BadRequestException (key "", message "User verification failed.") from the Admin Console organization delete-recover endpoint when _userService.VerifySecretAsync(user, model.Secret) returns false. The endpoint requires an authenticated admin to re-confirm their master password / client secret before a destructive org-delete-recover step. A deliberate 2-second Task.Delay precedes the throw to blunt timing attacks that would distinguish 'wrong secret' from 'valid secret'.
Source
Thrown at src/Api/AdminConsole/Controllers/OrganizationsController.cs:312
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.VerifySecretAsync(user, model.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
if (organization.IsValidClient())
{
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
if (provider.IsBillable())
{
await _providerBillingService.ScaleSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}
await _organizationDeleteCommand.DeleteAsync(organization);
}
View on GitHub (pinned to e93b962371)
Solutions
- Re-prompt the admin for their master password and re-derive MasterPasswordHash with the current KDF settings before resubmitting.
- Verify the request is being made by the same authenticated principal whose secret is being checked (no session/user mismatch).
- Check the client is sending model.Secret in the exact field the endpoint expects (Secret, not MasterPasswordHash) and that it is the PBKDF2/Argon2-derived hash, not the raw password.
- If the user recently changed their password or KDF, force a client relock/re-auth so the derived hash matches the server verifier.
Example fix
// before: sending raw password as the secret
body.Secret = rawPassword;
// after: derive the master-password hash client-side first
body.Secret = await crypto.hashPassword(rawPassword, user.kdf);
await api.post(`organizations/${id}/delete-recover`, body); Defensive patterns
Strategy: try-catch
Validate before calling
// Cannot know server-side correctness ahead of time, but validate shape
function validSecretPayload(secret) {
return typeof secret === 'string' && secret.length > 0 && secret.length < 1024;
} Type guard
function isSecretVerificationModel(v): v is { secret: string } {
return !!v && typeof v.secret === 'string' && v.secret.length > 0;
} Try / catch
try {
await api.post(`organizations/${id}/delete-recover`, body);
} catch (e) {
if (e?.response?.status === 400 && /User verification failed/i.test(e.response.data?.ValidationErrors?.['']?.[0] ?? '')) {
promptForMasterPasswordAgain();
} else throw e;
} Prevention
- Derive MasterPasswordHash client-side with the user's current KDF before sending.
- Force a relock/re-auth after a KDF or password change so the derived hash matches.
- Re-prompt for the master password immediately before destructive org operations.
When it happens
Trigger: POST to the organization delete-recover flow with a valid session (user resolved from principal) and an organization that exists, but model.Secret (master password hash or client secret) does not match the stored verifier. The org was found and the user was found, but secret verification failed.
Common situations: Admin typed the wrong master password; the client sent a stale or malformed MasterPasswordHash; the user's KDF/memory key was rotated but the client sent the old hash; secret sent for the wrong user account (session principal mismatch).
Related errors
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/b81e0311918a04c5.
Report an issue: GitHub.