{"record":{"id":"004a1cbfa71c2254","repo":"bitwarden/server","slug":"all-existing-reset-password-keys-must-be-included","errorCode":null,"errorMessage":"All existing reset password keys must be included in the rotation.","messagePattern":"All existing reset password keys must be included in the rotation\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs","lineNumber":44,"sourceCode":"        }\n\n        var result = new List<OrganizationUser>();\n\n        var existing = await _organizationUserRepository.GetManyByUserAsync(user.Id);\n        if (existing == null || existing.Count == 0)\n        {\n            return result;\n        }\n\n        // Exclude any account recovery that do not have a key.\n        existing = existing.Where(o => OrganizationUser.IsValidResetPasswordKey(o.ResetPasswordKey)).ToList();\n\n        foreach (var ou in existing)\n        {\n            var organizationUser = resetPasswordKeys.FirstOrDefault(a => a.OrganizationId == ou.OrganizationId);\n            if (organizationUser == null)\n            {\n                throw new BadRequestException(\"All existing reset password keys must be included in the rotation.\");\n            }\n\n            // Should be migrated to: if (!OrganizationUser.IsValidResetPasswordKey(organizationUser.ResetPasswordKey))\n            // after https://bitwarden.atlassian.net/browse/PM-31001 is resolved\n            if (organizationUser.ResetPasswordKey == null)\n            {\n                throw new BadRequestException(\"Reset Password keys cannot be set to null during rotation.\");\n            }\n\n            ou.ResetPasswordKey = organizationUser.ResetPasswordKey;\n            result.Add(ou);\n        }\n\n        return result;\n    }\n}\n","sourceCodeStart":26,"sourceCodeEnd":61,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/KeyManagement/Validators/OrganizationUserRotationValidator.cs#L26-L61","documentation":"Thrown by OrganizationUserRotationValidator during key rotation. It loads every organization membership the user has where account-recovery (reset password) is already provisioned (ResetPasswordKey is valid), and requires the rotation request to include each one matched by OrganizationId. Omitting one would leave that org's recovery key encrypted under the old key, so the rotation is rejected.","triggerScenarios":"Key-rotation request whose reset-password-keys array omits an OrganizationId for which the user has a valid ResetPasswordKey. The user joined an org (or org recovery was enabled) after the client cached memberships; the client only listed orgs it recognized.","commonSituations":"User accepted an org invite or an admin enabled account recovery between the client's last sync and the rotation; client built the org list from local state; an OrganizationId was dropped.","solutions":["Fetch the user's current organization memberships (with reset-password keys) immediately before building the rotation payload.","Include every OrganizationId that has a valid ResetPasswordKey, re-encrypted with the new user key.","If account recovery should no longer apply, leave the org or have recovery disabled before rotating.","Validate the submitted OrganizationId set covers every provisioned-recovery membership before sending."],"exampleFix":"// before\nconst keys = knownOrgs.map(o => ({ organizationId: o.id, resetPasswordKey: reencrypt(o.key) }));\n\n// after\nconst me = await api.getMyOrgMemberships();\nconst keys = me.filter(o => o.resetPasswordKey).map(o => ({ organizationId: o.organizationId, resetPasswordKey: reencrypt(o.resetPasswordKey) }));","handlingStrategy":"validation","validationCode":"const memberships = (await api.getMyOrgMemberships()).filter(o => o.resetPasswordKey);\nconst submitted = new Set(payload.resetPasswordKeys.map(k => k.organizationId));\nconst missing = memberships.filter(o => !submitted.has(o.organizationId));\nif (missing.length) {\n  throw new Error(`Rotation is missing reset-password keys for orgs: ${missing.map(o => o.organizationId).join(', ')}`);\n}","typeGuard":"function isCompleteOrgUserRotation(existing: { organizationId: string; resetPasswordKey: string | null }[], submitted: { organizationId: string }[]): boolean {\n  const have = new Set(submitted.map(s => s.organizationId));\n  return existing.filter(o => o.resetPasswordKey != null).every(o => have.has(o.organizationId));\n}","tryCatchPattern":"try {\n  await api.rotateKey(payload);\n} catch (e) {\n  if (e.status === 400 && /reset password keys must be included/i.test(e.message)) {\n    await refreshOrgMemberships();\n    payload.resetPasswordKeys = memberships.map(o => ({ organizationId: o.organizationId, resetPasswordKey: reencrypt(o.resetPasswordKey) }));\n    return api.rotateKey(payload);\n  }\n  throw e;\n}","preventionTips":["Re-fetch org memberships with reset-password keys before building the payload.","Leave the org or have recovery disabled to exclude it from rotation.","Assert the submitted OrganizationId set covers every membership with a valid reset key."],"tags":["key-rotation","organization","account-recovery","encryption","validation","bad-request"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}