passbolt/passbolt_api · error · BadRequestException

Single sign-on failed. Invalid nonce.

Error message

Single sign-on failed. Invalid nonce.

What it means

The OIDC nonce sent in the initial authorization request is stored in the SsoState record. When the provider returns the id_token, its nonce claim must match the stored value; a mismatch means the response may be replayed or not originate from this login attempt, so a BadRequestException is thrown.

Solutions

  1. Retry the SSO login from a fresh page (avoid stale bookmarked/cached authorization URLs).
  2. Check the provider's id_token JWT payload (decode it) to confirm the nonce claim is present and matches what was sent.
  3. Verify the provider implementation's getNonce() parses the id_token nonce claim correctly (custom providers).
  4. Clear cached SSO callback redirects and ensure only one login tab is used per attempt.

Example fix

// before (custom resource owner never reads nonce claim)
public function getNonce(): ?string { return null; }
// after
public function getNonce(): ?string { return $this->data['nonce'] ?? null; }
Defensive patterns

Strategy: retry

Validate before calling

// decode id_token locally before asserting
$claims = json_decode(base64_decode(explode('.', $idToken)[1]), true);
if (($claims['nonce'] ?? null) !== $ssoState->nonce) {
    // restart the SSO flow with a fresh state
}

Type guard

function nonceMatches(?string $idTokenNonce, SsoState $state): bool {
    return is_string($idTokenNonce) && hash_equals($state->nonce, $idTokenNonce);
}

Try / catch

try {
    $uac = $service->assertStateCodeAndGetUac($state, $code, ...);
} catch (BadRequestException $e) {
    if (str_contains($e->getMessage(), 'Invalid nonce')) {
        // start a brand-new SSO attempt (fresh authorization URL)
    }
    throw $e;
}

Prevention

When it happens

Trigger: During assertResourceOwnerAgainstSsoState, resourceOwner->getNonce() differs from $ssoState->nonce — e.g. the id_token nonce claim is missing/different, or the state token was reused from a previous login attempt.

Common situations: Provider does not echo the nonce in the id_token; the browser replayed an old authorization URL or a cached callback; multiple tabs started different SSO flows and states got crossed; provider SDK's resource-owner nonce extraction returns null.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at plugins/PassboltEe/Sso/src/Service/Sso/AbstractSsoService.php:305

        if (mb_strtolower($resourceOwner->getEmail()) !== mb_strtolower($user->username)) {
            $msg = __('Single sign-on failed.') . ' ' . __('Username mismatch.');
            throw new BadRequestException($msg);
        }
    }

    /**
     * @param \Passbolt\Sso\Utility\OpenId\SsoResourceOwnerInterface $resourceOwner Resource owner.
     * @param \Passbolt\Sso\Model\Entity\SsoState $ssoState SSO state.
     * @return void
     * @throws \Cake\Http\Exception\BadRequestException if the assertion failed
     */
    public function assertResourceOwnerAgainstSsoState(
        SsoResourceOwnerInterface $resourceOwner,
        SsoState $ssoState
    ): void {
        if ($ssoState->nonce !== $resourceOwner->getNonce()) {
            $msg = __('Single sign-on failed.') . ' ' . __('Invalid nonce.');
            throw new BadRequestException($msg);
        }
    }

    /**
     * @param string $state uuid
     * @param \App\Utility\ExtendedUserAccessControl $uac extend user access control
     * @param string $settingsId uuid
     * @param string $type Type of state.
     * @return \Passbolt\Sso\Model\Entity\SsoState
     */
    public function createSsoState(
        string $state,
        ExtendedUserAccessControl $uac,
        string $settingsId,
        string $type
    ): SsoState {
        return (new SsoStatesSetService())->create(
            $this->nonce,

View on GitHub (pinned to 31c1bbc10f)