bitwarden/server · error · BadRequestException
User has existing keypair
Error message
User has existing keypair
What it means
In POST /accounts/keys, if the user already has a PrivateKey or PublicKey set, the controller throws BadRequestException("User has existing keypair") → HTTP 400. Keys are set once; a second attempt is rejected to prevent overwriting an established keypair.
Source
Thrown at src/Api/Auth/Controllers/AccountsController.cs:519
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
revisionDate = CoreHelpers.ToEpocMilliseconds(date);
}
return revisionDate;
}
[HttpPost("keys")]
public async Task<KeysResponseModel> PostKeys([FromBody] KeysRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))
{
throw new BadRequestException("User has existing keypair");
}
if (model.AccountKeys != null)
{
var accountKeysData = model.AccountKeys.ToAccountKeysData();
if (!accountKeysData.IsV2Encryption())
{
throw new BadRequestException("AccountKeys are only supported for V2 encryption.");
}
// A client that predates the key id field sends none. The account then picks one up from
// the backfill endpoint on a later sync rather than here.
var userKeyId = KeyId.FromHexEncodedString(model.UserKeyId);
var updateUserDataTasks = userKeyId == null
? null
: new UpdateUserData[] { _userRepository.SetUserKeyId(user.Id, userKeyId) };
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData,
updateUserDataTasks);View on GitHub (pinned to e93b962371)
Solutions
- Check whether the user already has a keypair via GET /accounts/keys before attempting to set keys.
- Treat 'User has existing keypair' as a non-fatal condition and continue onboarding.
- If a genuine overwrite is required, use the key-rotation/update path rather than POST /accounts/keys.
- Add idempotency: skip the POST when the local profile already shows PublicKey set.
Example fix
// before
await client.PostAsync("accounts/keys", keysContent); // 400 if already set
// after
var existing = await client.GetFromJsonAsync<KeysResponseModel>("accounts/keys");
if (string.IsNullOrEmpty(existing?.PublicKey))
await client.PostAsync("accounts/keys", keysContent); Defensive patterns
Strategy: type-guard
Validate before calling
// Only POST keys when no keypair exists yet
var existing = await client.GetFromJsonAsync<KeysResponseModel>("accounts/keys");
var hasKeypair = !string.IsNullOrEmpty(existing?.PublicKey) || !string.IsNullOrEmpty(existing?.PrivateKey); Type guard
static bool NeedsKeypair(KeysResponseModel? k) =>
k is null || (string.IsNullOrEmpty(k.PublicKey) && string.IsNullOrEmpty(k.PrivateKey)); Try / catch
try { await client.PostAsync("accounts/keys", content); }
catch (BadRequestException ex) when (ex.Message.Contains("existing keypair"))
{ /* already set — not an error, continue onboarding */ } Prevention
- GET /accounts/keys before POST to check for an existing keypair.
- Treat 'existing keypair' as idempotent-success, not a hard failure.
- Use key rotation to change keys, never re-POST.
When it happens
Trigger: Calling POST /accounts/keys for a user who previously completed keypair setup; a retry after a partially-successful first call that already persisted the public/private key.
Common situations: Client retried an onboarding step that already succeeded; duplicate account-setup job; user re-running key import; race where two key-setup requests both passed the null check but the first committed.
Related errors
- AccountKeys are only supported for V2 encryption.
- The model state is invalid.
- You cannot delete accounts owned by an organization. Contact
- You must be authenticated to create a request of that type.
- Invalid AuthRequestType. Expected AdminApproval.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/410f675413ed6ddd.
Report an issue: GitHub.