{"record":{"id":"7b0e1e7135f19025","repo":"passbolt/passbolt_api","slug":"the-sso-state-does-not-exist","errorCode":null,"errorMessage":"The SSO state does not exist.","messagePattern":"The SSO state does not exist\\.","errorType":"http","errorClass":"RecordNotFoundException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/Sso/src/Service/SsoStates/SsoStatesGetService.php","lineNumber":50,"sourceCode":"     * @throws \\Cake\\Http\\Exception\\BadRequestException If given SSO state is invalid.\n     */\n    public function getOrFail(string $state): SsoState\n    {\n        if (!SsoState::isValidState($state)) {\n            throw new BadRequestException(__('The SSO state is invalid.'));\n        }\n\n        /** @var \\Passbolt\\Sso\\Model\\Table\\SsoStatesTable $ssoStatesTable */\n        $ssoStatesTable = $this->fetchTable('Passbolt/Sso.SsoStates');\n\n        try {\n            /** @var \\Passbolt\\Sso\\Model\\Entity\\SsoState $ssoState */\n            $ssoState = $ssoStatesTable\n                ->find('active')\n                ->where(['state' => $state])\n                ->firstOrFail();\n        } catch (RecordNotFoundException $e) {\n            throw new RecordNotFoundException(__('The SSO state does not exist.'), 400, $e);\n        }\n\n        return $ssoState;\n    }\n}\n","sourceCodeStart":32,"sourceCodeEnd":56,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/Sso/src/Service/SsoStates/SsoStatesGetService.php#L32-L56","documentation":"The state token is well-formed but no active (non-deleted, non-expired) SsoState row matches it in the sso_states table; firstOrFail() raises RecordNotFoundException which is re-thrown with this message and code 400. States are single-use: consume() marks them deleted immediately after assertion, so a used state also produces this error.","triggerScenarios":"Callback arrives with a state whose row is absent from sso_states (expired and purged, already consumed by a previous callback, or created on a different server/database), or the find('active') filter excludes it because deleted is set.","commonSituations":"User double-submits or refreshes the callback page after the state was consumed; load-balanced passbolt instances writing to different databases so the initiating server's state is unknown to the callback server; SSO flow left open past expiry and cleanup removed the row; browser back-button replay after a completed login.","solutions":["Restart the SSO login flow to generate a new state — a consumed or expired state can never be reused.","If load-balanced, ensure all instances share the same database (or sticky sessions/consistent routing for the SSO round-trip).","Do not refresh or replay the callback URL; each state is single-use by design.","Check the sso_states table for the state value to confirm whether it was consumed (deleted set) or never created on that DB.","Increase retention/cleanup window if states are being purged while users are still mid-flow (e.g. slow login pages, long manual approval steps)."],"exampleFix":"# before: replayed callback URL after login completed -> state already deleted\n# after: always start a fresh SSO flow\nGET /sso/<provider>/login  ->  new state  ->  callback once","handlingStrategy":"try-catch","validationCode":"// Pre-check that the state still exists and is active\n$exists = TableRegistry::getTableLocator()->get('Passbolt/Sso.SsoStates')\n    ->find('active')->where(['state' => $state])->count() > 0;\nif (!$exists) {\n    // expired/consumed/unknown: restart SSO flow before calling the API\n}","typeGuard":"function isActiveState(?SsoState $ssoState): bool {\n    return $ssoState !== null && $ssoState->deleted === null && !$ssoState->isExpired();\n}","tryCatchPattern":"try {\n    $ssoState = $getService->getOrFail($state);\n} catch (RecordNotFoundException $e) {\n    // state unknown, consumed, or expired: start a new SSO login flow\n    throw $e;\n}","preventionTips":["Never reuse or replay a callback URL; states are single-use.","Point all load-balanced instances at the same database.","Complete the SSO round-trip promptly, before expiry/cleanup.","Do not use the browser back button after successful SSO login."],"tags":["sso","oauth-state","not-found","single-use-token","expired-state"],"backgroundTag":"record-not-found","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"}