{"record":{"id":"d6fe287770da6def","repo":"passbolt/passbolt_api","slug":"the-sso-authentication-token-is-invalid-user-agent-mismatch","errorCode":null,"errorMessage":"The SSO authentication token is invalid. User agent mismatch.","messagePattern":"The SSO authentication token is invalid\\. User agent mismatch\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"plugins/PassboltEe/Sso/src/Service/SsoAuthenticationTokens/SsoAuthenticationTokenGetService.php","lineNumber":206,"sourceCode":"            try {\n                $ip = $token->getDataProperty(SsoAuthenticationToken::DATA_IP);\n            } catch (AuthenticationTokenDataPropertyException $exception) {\n                throw new BadRequestException($errorMsg . __('Token IP is missing.'), 400, $exception);\n            }\n\n            if ($ip !== $uac->getUserIp()) {\n                throw new BadRequestException($errorMsg . __('User IP mismatch.'));\n            }\n        }\n\n        if (Configure::read('passbolt.security.userAgent')) {\n            try {\n                $ua = $token->getDataProperty(SsoAuthenticationToken::DATA_USER_AGENT);\n            } catch (AuthenticationTokenDataPropertyException $exception) {\n                throw new BadRequestException($errorMsg . __('User agent is missing.'), 400, $exception);\n            }\n            if ($ua !== $uac->getUserAgent()) {\n                throw new BadRequestException($errorMsg . __('User agent mismatch.'));\n            }\n        }\n\n        if ($sid !== $settingsId || !Validation::uuid($sid)) {\n            throw new BadRequestException($errorMsg . __('Settings mismatch.'));\n        }\n    }\n}\n","sourceCodeStart":188,"sourceCodeEnd":215,"githubUrl":"https://github.com/passbolt/passbolt_api/blob/31c1bbc10f32808a607fa9bd81891e898779c0bc/plugins/PassboltEe/Sso/src/Service/SsoAuthenticationTokens/SsoAuthenticationTokenGetService.php#L188-L215","documentation":"Passbolt's SSO authentication token service validates that the browser user agent stored in the SSO authentication token data matches the user agent of the current request before consuming the token. When they differ, it concludes the token is being replayed from a different client and throws BadRequestException with 'User agent mismatch.' This protects the draft/SSO login flow against token theft across devices.","triggerScenarios":"Calling SsoAuthenticationTokenGetService::assertAndConsume() (via the SSO draft/key retrieval endpoints) with a token whose DATA_USER_AGENT data property differs from the current request's User-Agent header, e.g. the SSO flow was started in one browser and the token is redeemed from another client or an HTTP client/curl call.","commonSituations":"A backend integration or test harness (curl, Postman, CI script) redeems the SSO token without reproducing the browser's exact User-Agent string; a proxy or middleware rewrites the User-Agent between the two SSO steps; the user switches browsers mid-login; token generated server-side in tests with a different UA.","solutions":["Redeem the SSO authentication token with the exact same User-Agent header that started the SSO flow","If done programmatically (curl/SDK), copy the browser's User-Agent header into the token retrieval request","Restart the SSO login flow from the same client so a fresh token is issued with the current user agent","Check reverse-proxy/CDN config for User-Agent rewriting between requests","In tests, set the request user agent explicitly to match the one used at token creation"],"exampleFix":"// before (curl, different UA)\ncurl -H 'Authorization: <token>' https://passbolt/sso/keys\n// after\ncurl -H 'Authorization: <token>' -H 'User-Agent: Mozilla/5.0 (same UA that started the SSO flow)' https://passbolt/sso/keys","handlingStrategy":"try-catch","validationCode":"// client: ensure UA consistency before redeeming\nconst ua = navigator.userAgent;\nif (storedUa !== ua) restartSsoFlow();","typeGuard":null,"tryCatchPattern":"try {\n    $service->assertAndConsume($token, $uac, $settingsId);\n} catch (BadRequestException $e) {\n    if (str_contains($e->getMessage(), 'User agent mismatch')) {\n        return $this->restartSsoFlow(); // new token with current UA\n    }\n    throw $e;\n}","preventionTips":["Always redeem SSO tokens from the same browser/client that started the flow","Copy the originating User-Agent header into programmatic requests","Disable proxies/middleware that rewrite User-Agent between SSO steps"],"tags":["sso","authentication-token","user-agent-mismatch","bad-request"],"backgroundTag":"invalid-argument-value","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"}