{"record":{"id":"410f675413ed6ddd","repo":"bitwarden/server","slug":"user-has-existing-keypair","errorCode":null,"errorMessage":"User has existing keypair","messagePattern":"User has existing keypair","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Auth/Controllers/AccountsController.cs","lineNumber":519,"sourceCode":"            var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);\n            revisionDate = CoreHelpers.ToEpocMilliseconds(date);\n        }\n\n        return revisionDate;\n    }\n\n    [HttpPost(\"keys\")]\n    public async Task<KeysResponseModel> PostKeys([FromBody] KeysRequestModel model)\n    {\n        var user = await _userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        if (!string.IsNullOrWhiteSpace(user.PrivateKey) || !string.IsNullOrWhiteSpace(user.PublicKey))\n        {\n            throw new BadRequestException(\"User has existing keypair\");\n        }\n\n        if (model.AccountKeys != null)\n        {\n            var accountKeysData = model.AccountKeys.ToAccountKeysData();\n            if (!accountKeysData.IsV2Encryption())\n            {\n                throw new BadRequestException(\"AccountKeys are only supported for V2 encryption.\");\n            }\n            // A client that predates the key id field sends none. The account then picks one up from\n            // the backfill endpoint on a later sync rather than here.\n            var userKeyId = KeyId.FromHexEncodedString(model.UserKeyId);\n            var updateUserDataTasks = userKeyId == null\n                ? null\n                : new UpdateUserData[] { _userRepository.SetUserKeyId(user.Id, userKeyId) };\n\n            await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, accountKeysData,\n                updateUserDataTasks);","sourceCodeStart":501,"sourceCodeEnd":537,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Auth/Controllers/AccountsController.cs#L501-L537","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait client.PostAsync(\"accounts/keys\", keysContent); // 400 if already set\n// after\nvar existing = await client.GetFromJsonAsync<KeysResponseModel>(\"accounts/keys\");\nif (string.IsNullOrEmpty(existing?.PublicKey))\n    await client.PostAsync(\"accounts/keys\", keysContent);","handlingStrategy":"type-guard","validationCode":"// Only POST keys when no keypair exists yet\nvar existing = await client.GetFromJsonAsync<KeysResponseModel>(\"accounts/keys\");\nvar hasKeypair = !string.IsNullOrEmpty(existing?.PublicKey) || !string.IsNullOrEmpty(existing?.PrivateKey);","typeGuard":"static bool NeedsKeypair(KeysResponseModel? k) =>\n    k is null || (string.IsNullOrEmpty(k.PublicKey) && string.IsNullOrEmpty(k.PrivateKey));","tryCatchPattern":"try { await client.PostAsync(\"accounts/keys\", content); }\ncatch (BadRequestException ex) when (ex.Message.Contains(\"existing keypair\"))\n{ /* already set — not an error, continue onboarding */ }","preventionTips":["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."],"tags":["validation","bad-request","keys","keypair","idempotency"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}