{"record":{"id":"882bc924763e627e","repo":"passbolt/passbolt_api","slug":"single-sign-on-failed-invalid-nonce","errorCode":null,"errorMessage":"Single sign-on failed. Invalid nonce.","messagePattern":"Single sign-on failed\\. Invalid nonce\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/Sso/src/Service/Sso/AbstractSsoService.php","lineNumber":305,"sourceCode":"        if (mb_strtolower($resourceOwner->getEmail()) !== mb_strtolower($user->username)) {\n            $msg = __('Single sign-on failed.') . ' ' . __('Username mismatch.');\n            throw new BadRequestException($msg);\n        }\n    }\n\n    /**\n     * @param \\Passbolt\\Sso\\Utility\\OpenId\\SsoResourceOwnerInterface $resourceOwner Resource owner.\n     * @param \\Passbolt\\Sso\\Model\\Entity\\SsoState $ssoState SSO state.\n     * @return void\n     * @throws \\Cake\\Http\\Exception\\BadRequestException if the assertion failed\n     */\n    public function assertResourceOwnerAgainstSsoState(\n        SsoResourceOwnerInterface $resourceOwner,\n        SsoState $ssoState\n    ): void {\n        if ($ssoState->nonce !== $resourceOwner->getNonce()) {\n            $msg = __('Single sign-on failed.') . ' ' . __('Invalid nonce.');\n            throw new BadRequestException($msg);\n        }\n    }\n\n    /**\n     * @param string $state uuid\n     * @param \\App\\Utility\\ExtendedUserAccessControl $uac extend user access control\n     * @param string $settingsId uuid\n     * @param string $type Type of state.\n     * @return \\Passbolt\\Sso\\Model\\Entity\\SsoState\n     */\n    public function createSsoState(\n        string $state,\n        ExtendedUserAccessControl $uac,\n        string $settingsId,\n        string $type\n    ): SsoState {\n        return (new SsoStatesSetService())->create(\n            $this->nonce,","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/Sso/src/Service/Sso/AbstractSsoService.php#L287-L323","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the SSO login from a fresh page (avoid stale bookmarked/cached authorization URLs).","Check the provider's id_token JWT payload (decode it) to confirm the nonce claim is present and matches what was sent.","Verify the provider implementation's getNonce() parses the id_token nonce claim correctly (custom providers).","Clear cached SSO callback redirects and ensure only one login tab is used per attempt."],"exampleFix":"// before (custom resource owner never reads nonce claim)\npublic function getNonce(): ?string { return null; }\n// after\npublic function getNonce(): ?string { return $this->data['nonce'] ?? null; }","handlingStrategy":"retry","validationCode":"// decode id_token locally before asserting\n$claims = json_decode(base64_decode(explode('.', $idToken)[1]), true);\nif (($claims['nonce'] ?? null) !== $ssoState->nonce) {\n    // restart the SSO flow with a fresh state\n}","typeGuard":"function nonceMatches(?string $idTokenNonce, SsoState $state): bool {\n    return is_string($idTokenNonce) && hash_equals($state->nonce, $idTokenNonce);\n}","tryCatchPattern":"try {\n    $uac = $service->assertStateCodeAndGetUac($state, $code, ...);\n} catch (BadRequestException $e) {\n    if (str_contains($e->getMessage(), 'Invalid nonce')) {\n        // start a brand-new SSO attempt (fresh authorization URL)\n    }\n    throw $e;\n}","preventionTips":["Never bookmark or reuse old authorization URLs; always start a fresh SSO attempt.","Complete the flow in a single tab to avoid crossed states.","Ensure custom provider classes read the nonce claim from the id_token.","Retry promptly; don't let state tokens linger before callback."],"tags":["sso","oidc","nonce","csrf"],"backgroundTag":"checksum-mismatch","analyzedSha":"31c1bbc10f32808a607fa9bd81891e898779c0bc","analyzedAt":"2026-09-17T00:04:38.960Z","contentChangedAt":"2026-09-17T00:04:38.960Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}