{"record":{"id":"2f72b975a56426b8","repo":"passbolt/passbolt_api","slug":"invalid-request-keys-are-required-for-this-change","errorCode":null,"errorMessage":"Invalid request. Keys are required for this change.","messagePattern":"Invalid request\\. Keys are required for this change\\.","errorType":"http","errorClass":"Cake\\Http\\Exception\\BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryOrganizationPolicies/AccountRecoveryOrganizationPolicySetService.php","lineNumber":92,"sourceCode":"            return $this->enablePolicy($uac, $newPolicy);\n        }\n\n        // if enabled => disabled\n        if ($this->isDisabling()) {\n            // if new key or passwords provided\n            if ($isNewKeyProvided || $isPrivateKeyPasswordsProvided) {\n                throw new BadRequestException(__('Invalid request. New key or passwords are not required.'));\n            }\n\n            // save new disabled policy, disable previous key and delete backups if any\n            return $this->disablePolicy($uac);\n        }\n\n        // if enabled => enabled\n        // e.g it's policy change like mandatory => opt-in\n        // and/or a possible key rotation\n        if (($isNewKeyProvided && !$isRevokedKeyProvided) || (!$isNewKeyProvided && $isRevokedKeyProvided)) {\n            throw new BadRequestException(__('Invalid request. Keys are required for this change.'));\n        }\n\n        // if key provided or revocation provided\n        $newKey = null;\n        $oldKey = null;\n        $passwords = null;\n        /** @psalm-suppress RedundantCondition */\n        if ($isNewKeyProvided && $isRevokedKeyProvided) {\n            // assert old and new key$newKey\n            $newKey = $this->buildPublicKeyEntityFromDataOrFail($uac);\n            $oldKey = $this->buildRevokedKeyEntityFromDataOrFail($uac);\n\n            // If some existing backups are present\n            // assert new backups are provided\n            if ($this->backupsExists()) {\n                if (!$isPrivateKeyPasswordsProvided) {\n                    throw new BadRequestException(__('Invalid request. Passwords are required for this change.'));\n                }","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/AccountRecovery/src/Service/AccountRecoveryOrganizationPolicies/AccountRecoveryOrganizationPolicySetService.php#L74-L110","documentation":"This BadRequestException is thrown when an enabled-to-enabled policy change request provides only one half of a key rotation: a new public key without the revoked (old) key, or a revoked key without the new key. Key rotation and policy changes (e.g. mandatory to opt-in) must be accompanied by a consistent pair of keys, or by neither when only the policy changes and the key is reused.","triggerScenarios":"POST/PUT to the account recovery organization settings with current policy enabled and new policy enabled, where exactly one of 'account_recovery_organization_public_key' (new key) or 'account_recovery_organization_revoked_key' is present but not both. Raised in AccountRecoveryOrganizationPolicySetService::set() at line 92.","commonSituations":"A client implements key rotation but forgets to mark the old key as revoked; an admin wants to change the policy (mandatory to opt-in) but the client library injects a new key without the revocation record; partially migrated request builders after an API version change.","solutions":["If rotating the key: provide BOTH the new armored public key and the old key marked as revoked in the same request.","If only changing the policy (e.g. mandatory to opt-in) with no rotation: provide NEITHER key and instead pass the current public_key_id so it can be reused.","Audit the client payload to ensure the new-key and revoked-key fields are always set together or not at all.","Use the official passbolt client/CLI for organization policy changes instead of hand-built requests."],"exampleFix":"// before (new key without revocation)\nawait passbolt.setAccountRecoveryOrganizationPolicy({\n  policy: 'opt-in',\n  account_recovery_organization_public_key: newArmoredKey\n});\n\n// after (complete rotation: new key + revoked old key)\nawait passbolt.setAccountRecoveryOrganizationPolicy({\n  policy: 'opt-in',\n  account_recovery_organization_public_key: newArmoredKey,\n  account_recovery_organization_revoked_key: oldArmoredKey\n});","handlingStrategy":"validation","validationCode":"const hasNew = !!payload.account_recovery_organization_public_key;\nconst hasRevoked = !!payload.account_recovery_organization_revoked_key;\nif (hasNew !== hasRevoked) {\n  throw new Error('Key rotation requires both the new public key and the revoked key, or neither.');\n}","typeGuard":"function hasCompleteKeyPair(p) {\n  const hasNew = 'account_recovery_organization_public_key' in p && p.account_recovery_organization_public_key;\n  const hasRevoked = 'account_recovery_organization_revoked_key' in p && p.account_recovery_organization_revoked_key;\n  return hasNew === hasRevoked;\n}","tryCatchPattern":"try {\n  await passbolt.setAccountRecoveryOrganizationPolicy(payload);\n} catch (e) {\n  if (e.status === 400 && /Keys are required for this change/.test(e.message)) {\n    console.error('Provide both new key and revoked key, or neither (policy-only change with public_key_id).');\n  }\n  throw e;\n}","preventionTips":["Treat new key and revoked key as a single atomic unit in client code — validate their presence together before sending.","For policy-only changes, intentionally omit both keys and send public_key_id.","Write an integration test covering both rotation and policy-only-change payload shapes."],"tags":["api","bad-request","key-rotation","account-recovery"],"backgroundTag":"missing-required-argument","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}