passbolt/passbolt_api · error · RecordNotFoundException
The SSO state does not exist.
Error message
The SSO state does not exist.
What it means
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.
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).
Example fix
# before: replayed callback URL after login completed -> state already deleted # after: always start a fresh SSO flow GET /sso/<provider>/login -> new state -> callback once
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check that the state still exists and is active
$exists = TableRegistry::getTableLocator()->get('Passbolt/Sso.SsoStates')
->find('active')->where(['state' => $state])->count() > 0;
if (!$exists) {
// expired/consumed/unknown: restart SSO flow before calling the API
} Type guard
function isActiveState(?SsoState $ssoState): bool {
return $ssoState !== null && $ssoState->deleted === null && !$ssoState->isExpired();
} Try / catch
try {
$ssoState = $getService->getOrFail($state);
} catch (RecordNotFoundException $e) {
// state unknown, consumed, or expired: start a new SSO login flow
throw $e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- The authentication token does not exist.
- Could not save the SSO state, invalid nonce.
- The authentication token does not exist or has been deleted.
- The SSO key does not exist.
- The SSO key does not exist.
AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17).
Data as JSON: /api/errors/7b0e1e7135f19025.
Report an issue: GitHub.
Appendix: source
Thrown at plugins/PassboltEe/Sso/src/Service/SsoStates/SsoStatesGetService.php:50
* @throws \Cake\Http\Exception\BadRequestException If given SSO state is invalid.
*/
public function getOrFail(string $state): SsoState
{
if (!SsoState::isValidState($state)) {
throw new BadRequestException(__('The SSO state is invalid.'));
}
/** @var \Passbolt\Sso\Model\Table\SsoStatesTable $ssoStatesTable */
$ssoStatesTable = $this->fetchTable('Passbolt/Sso.SsoStates');
try {
/** @var \Passbolt\Sso\Model\Entity\SsoState $ssoState */
$ssoState = $ssoStatesTable
->find('active')
->where(['state' => $state])
->firstOrFail();
} catch (RecordNotFoundException $e) {
throw new RecordNotFoundException(__('The SSO state does not exist.'), 400, $e);
}
return $ssoState;
}
}
View on GitHub (pinned to 31c1bbc10f)