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

  1. Check whether the user already has a keypair via GET /accounts/keys before attempting to set keys.
  2. Treat 'User has existing keypair' as a non-fatal condition and continue onboarding.
  3. If a genuine overwrite is required, use the key-rotation/update path rather than POST /accounts/keys.
  4. 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

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


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