{"record":{"id":"7b4c4298aca053bc","repo":"passbolt/passbolt_api","slug":"account-recovery-is-disabled-key-backup-is-not-supported","errorCode":null,"errorMessage":"Account recovery is disabled. Key backup is not supported.","messagePattern":"Account recovery is disabled\\. Key backup is not supported\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/AccountRecovery/src/Service/Setup/AccountRecoverySetupCompleteService.php","lineNumber":130,"sourceCode":"\n                return $this->saveUserEntity($user, $saveOptions);\n            }\n        );\n    }\n\n    /**\n     * Assert that there is not too much or not enough data\n     * Mandatory: both private key and password must be provided\n     * Disabled: none of them must be provided\n     *\n     * @return void\n     * @throws \\Cake\\Http\\Exception\\BadRequestException if data is missing or too much data is sent\n     */\n    protected function assertRequestSanity(): void\n    {\n        if ($this->policy->isDisabled()) {\n            if ($this->isAccountRecoveryUserSettingProvided()) {\n                throw new BadRequestException(__('Account recovery is disabled. Key backup is not supported.'));\n            }\n        } elseif ($this->policy->isMandatory()) {\n            if (!$this->isPrivateKeyProvided() || !$this->arePasswordsProvided()) {\n                throw new BadRequestException(\n                    __('Account recovery is mandatory. Please provide the mandatory data.')\n                );\n            }\n        }\n    }\n\n    /**\n     * @return bool true if the account_recovery_user_setting data is set\n     */\n    protected function isAccountRecoveryUserSettingProvided(): bool\n    {\n        return is_array($this->request->getData('account_recovery_user_setting'));\n    }\n","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/AccountRecovery/src/Service/Setup/AccountRecoverySetupCompleteService.php#L112-L148","documentation":"Thrown in AccountRecoverySetupCompleteService::assertRequestSanity when the organization account recovery policy is 'disabled' yet the client still sends account recovery user setting data (e.g. a recovery preference) in the setup complete payload. With recovery disabled, key backup is unsupported, so any recovery-related payload is rejected as a bad request.","triggerScenarios":"POST /setup/setup/complete (or account recovery setup complete) where the org policy is disabled and the request body contains account_recovery_user_setting (or related recovery fields).","commonSituations":"Organization administrator disabled account recovery after the client UI was loaded (stale UI still offers 'save my key on server'); a cached/stale front-end bundle posting legacy recovery fields; misconfigured environment where the EE account recovery setting was toggled off mid-setup.","solutions":["Remove the account recovery user setting data from the setup complete request payload and retry","Reload the setup page so the client fetches the current organization recovery policy and hides backup options","If backup should be available, re-enable the account recovery policy in the admin settings (with correct EE license)","Clear browser cache / update the passbolt web extension to the version matching the server policy"],"exampleFix":"// before\npayload = { ...baseData, account_recovery_user_setting: 'enabled' };\n// after (policy disabled)\npayload = { ...baseData }; // no recovery fields when policy is disabled","handlingStrategy":"validation","validationCode":"const policy = await getOrgPolicy();\nif (policy === 'disabled' && 'account_recovery_user_setting' in payload) {\n  delete payload.account_recovery_user_setting;\n}","typeGuard":"const recoveryDataProvided = (p) => p != null && ('account_recovery_user_setting' in p || 'account_recovery_private_key' in p);","tryCatchPattern":"try {\n  await setupComplete(payload);\n} catch (e) {\n  if (e.code === 400 && /disabled/.test(e.message)) {\n    payload = stripRecoveryFields(payload);\n    await setupComplete(payload);\n  } else { throw e; }\n}","preventionTips":["Refresh the organization recovery policy before rendering the setup UI","Keep browser extension and server versions in sync","Strip recovery fields from payloads when policy is 'disabled'","Reload the page after admin changes to recovery settings"],"tags":["account-recovery","policy","http-400","stale-payload"],"backgroundTag":"conflicting-config-options","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"}