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
- Restart the SSO flow in the same browser/tab that initiated it, with cookies enabled for the passbolt domain.
- 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.
- Do not copy/paste the callback URL into another browser or incognito window.
- 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
- Keep cookies enabled and same-browser for the entire SSO flow
- Never open the provider callback link in a different browser
- Check SameSite/Secure cookie attributes behind proxies or different domains
- Avoid running parallel SSO flows in one browser
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
- CSRF issue. The state in request data does not match with…
- The state is required in cookie.
- Single sign-on failed. Invalid nonce.
- The Duo state should match the authentication token state.
- The SSO state is invalid.
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)