passbolt/passbolt_api · critical · InternalErrorException

The SSO state could not be saved.

Error message

The SSO state could not be saved.

What it means

After assertions, consume() marks the SSO state as used by setting deleted = now() and saving it. If the save fails, an InternalErrorException (HTTP 500) is thrown. Note the state is only 'consumed' via the deleted timestamp (soft delete); the save is essential to make single-use states unusable, so a failure here is treated as a server fault.

Solutions

  1. Check database health and application error logs for the underlying save failure (connection refused, deadlock, disk full).
  2. Run pending migrations (passbolt migrate / ddev refresh) to ensure the sso_states schema exists and matches the code version.
  3. Retry the SSO flow once the database is available — the state may remain unconsumed; if in doubt, re-initiate.
  4. If it recurs on every callback, inspect SsoStatesTable validation/rules for a rule rejecting the save and check the entity's fields against the schema.

Example fix

# before: schema drift after upgrade
# after
ddev refresh   # or: bin/cake passbolt migrate
# verify sso_states table exists with 'deleted' column
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check DB availability before SSO callback processing
try {
    $conn = ConnectionManager::get('default');
    $conn->execute('SELECT 1 FROM sso_states LIMIT 1');
} catch (Throwable $e) {
    // DB unreachable: fail fast instead of a 500 mid-consume
}

Type guard

function isSavable(SsoState $ssoState): bool {
    return $ssoState->id !== null && $ssoState->deleted === null;
}

Try / catch

try {
    $service->assertAndConsume($ssoState, $settingsId, $uac);
} catch (InternalErrorException $e) {
    if ($e->getMessage() === 'The SSO state could not be saved.') {
        // check DB health / run migrations, then retry the flow
    }
    throw $e;
}

Prevention

When it happens

Trigger: SsoStatesTable->save($ssoState) returns false during consume(), called at the end of assertAndConsume/assertAndConsumeWithoutUser (both on success and when re-throwing an assertion failure). Typical causes: entity validation errors on save, database connection failure, missing sso_states table, or the entity marked dirty-but-invalid.

Common situations: Database outage or connection limit reached during an SSO callback; migration not run so the sso_states table or columns (deleted) are missing; write permissions/replication issues on the DB; lock/table corruption in MySQL/Postgres.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/c269933b8845fe9b. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltEe/Sso/src/Service/SsoStates/SsoStatesAssertService.php:169

        }
    }

    /**
     * Marks given state as deleted.
     *
     * @param \Passbolt\Sso\Model\Entity\SsoState $ssoState SSO state entity.
     * @return void
     * @throws \Cake\Http\Exception\InternalErrorException When unable to save the SSO state entity.
     */
    private function consume(SsoState $ssoState): void
    {
        /** @var \Passbolt\Sso\Model\Table\SsoStatesTable $ssoStatesTable */
        $ssoStatesTable = $this->fetchTable('Passbolt/Sso.SsoStates');

        $ssoState->deleted = DateTime::now();

        if (!$ssoStatesTable->save($ssoState)) {
            throw new InternalErrorException(__('The SSO state could not be saved.'));
        }
    }
}

View on GitHub (pinned to 31c1bbc10f)