passbolt/passbolt_api · error · BadRequestException

CSRF issue. The state in URL and Cookies do not match.

Error message

CSRF issue. The state in URL and Cookies do not match.

What it means

Standard CSRF protection for the SSO redirect flow: the 'state' query parameter from the provider's callback must exactly match the value stored in the SSO state cookie. A mismatch means the request was not initiated by this client (or cookies were lost), so the controller rejects it with a 400.

Solutions

  1. Restart the SSO flow in the same browser/tab that initiated it, with cookies enabled for the passbolt domain.
  2. Check cookie SameSite/Secure settings — if passbolt runs behind a different domain than the IdP redirect, third-party cookie blocking may drop the state cookie.
  3. Do not copy/paste the callback URL into another browser or incognito window.
  4. Clear stale passbolt SSO cookies and retry; multiple concurrent SSO attempts overwrite the cookie.
Defensive patterns

Strategy: validation

Validate before calling

const stateUrl = new URL(callbackUrl).searchParams.get('state');
const stateCookie = document.cookie.match(/passbolt_sso_state=([^;]+)/)?.[1];
if (stateUrl !== decodeURIComponent(stateCookie || '')) { throw new Error('SSO state mismatch — restart flow'); }

Type guard

function statesMatch(urlState, cookieState) { return typeof urlState === 'string' && typeof cookieState === 'string' && urlState === cookieState; }

Try / catch

try { await completeSsoCallback(url); } catch (e) { if (e.status === 400 && /CSRF issue/.test(e.message)) { clearSsoCookies(); restartLogin(); } else { throw e; } }

Prevention

When it happens

Trigger: GET on the SSO callback endpoint where getStateFromUrlAndCookie() compares query 'state' with the SSO_COOKIE state cookie and they differ (missing, expired, or different cookie).

Common situations: Browser blocking third-party/ SameSite cookies so the state cookie is absent on redirect back; opening the callback link in a different browser than the one that started the flow; proxy stripping cookies; multiple SSO tabs overwriting each other's state cookie.

Related errors


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

Appendix: source

Thrown at plugins/PassboltEe/Sso/src/Controller/AbstractSsoController.php:45

use Passbolt\Sso\Model\Dto\SsoUrlResponseDto;
use Passbolt\Sso\Model\Entity\SsoState;
use Passbolt\Sso\Service\Sso\AbstractSsoService;
use Passbolt\Sso\Utility\Validation\OAuthTokenValidation;

abstract class AbstractSsoController extends AppController
{
    /**
     * Protect from CSRF by checking if state in URL and cookie matches
     *
     * @throws \Cake\Http\Exception\BadRequestException if the state is not provided in cookie or URL or there is a mismatch
     * @return string
     */
    public function getStateFromUrlAndCookie(): string
    {
        $stateUrl = $this->getStateFromUrlQuery();
        $stateCookie = $this->getStateFromCookie();
        if ($stateUrl !== $stateCookie) {
            throw new BadRequestException(__('CSRF issue. The state in URL and Cookies do not match.'));
        }

        return $stateUrl;
    }

    /**
     * Protect from CSRF by checking if state in URL and cookie matches
     *
     * @throws \Cake\Http\Exception\BadRequestException if the state is not provided in cookie or URL or there is a mismatch
     * @return string
     */
    public function getStateAndAssertAgainstCookie(): string
    {
        $state = $this->getRequest()->getData('state');
        $stateCookie = $this->getStateFromCookie();
        if ($state !== $stateCookie) {
            throw new BadRequestException(
                __('CSRF issue. The state in request data does not match with cookie value.')

View on GitHub (pinned to 31c1bbc10f)