passbolt/passbolt_api · error · BadRequestException

The SSO state is invalid. User IP mismatch.

Error message

The SSO state is invalid. User IP mismatch.

What it means

Thrown by SsoStatesAssertService::assert when passbolt.security.userIp is enabled and the IP address stored in the SSO state differs from the current user's IP. The state is bound to the originating client IP to prevent the state being replayed from a different machine.

Solutions

  1. Retry the SSO login without changing network/VPN mid-flow.
  2. Configure trusted proxies so passbolt resolves the real client IP (security.trustedProxies / App.fullBaseUrl behind reverse proxy).
  3. If your deployment legitimately changes client IPs, verify whether disabling passbolt.security.userIp is acceptable security-wise.
Defensive patterns

Strategy: try-catch

Validate before calling

if ($stateRecord->ip !== $currentUserIp) { /* state will be rejected — restart login */ }

Try / catch

try { $svc->assertAndConsume($state, $settingsId, $uac); } catch (BadRequestException $e) { if (str_contains($e->getMessage(), 'User IP mismatch')) { /* restart flow */ } throw $e; }

Prevention

When it happens

Trigger: User starts SSO login on one network (e.g. Wi-Fi) and completes the IdP callback on another (e.g. mobile data / VPN toggle); reverse proxy not passing X-Forwarded-For so passbolt sees the proxy IP instead of the client IP.

Common situations: VPN connect/disconnect mid-login; load-balanced setups where client IP forwarding is misconfigured; users on mobile networks switching cells.

Related errors


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

Appendix: source

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

    private function assert(SsoState $ssoState, string $ssoSettingsId, ExtendedUserAccessControl $uac): void
    {
        $errorMsg = __('The SSO state is invalid.') . ' ';

        if (!SsoState::isValidState($ssoState->state)) {
            throw new BadRequestException(trim($errorMsg));
        }

        if ($ssoState->isExpired()) {
            throw new BadRequestException($errorMsg . __('The SSO state is expired.'));
        }

        if ($ssoState->user_id !== $uac->getId() || !Validation::uuid($ssoState->user_id)) {
            throw new BadRequestException($errorMsg . __('User id mismatch.'));
        }

        if (Configure::read('passbolt.security.userIp')) {
            if ($ssoState->ip !== $uac->getUserIp()) {
                throw new BadRequestException($errorMsg . __('User IP mismatch.'));
            }
        }

        if (Configure::read('passbolt.security.userAgent')) {
            if ($ssoState->user_agent !== $uac->getUserAgent()) {
                throw new BadRequestException($errorMsg . __('User agent mismatch.'));
            }
        }

        if ($ssoState->sso_settings_id !== $ssoSettingsId || !Validation::uuid($ssoState->sso_settings_id)) {
            throw new BadRequestException($errorMsg . __('Settings mismatch.'));
        }
    }

    /**
     * Same assertions but without user ID.
     *
     * @param \Passbolt\Sso\Model\Entity\SsoState $ssoState SSO state entity.

View on GitHub (pinned to 31c1bbc10f)