fix(auth): validate OAuth state and verify id_token in azure-ad-hybrid callback
Security fix: azure-ad-hybrid OAuth callback skips state validation and trusts a client-supplied id_token
Severity: CRITICAL · CVSS: 9.3 (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N) · CWE-347 · OWASP A04:2025 Cryptographic Failures
Summary
The azure-ad-hybrid login callback accepts an OAuth code and an id_token straight from the browser, never validates the OAuth state cookie, and never verifies the id_token before handing it to the application's oauth_callback. Because the framework passes the token through untouched, the identity used to mint the session is whatever the caller wrote into that form field.
oauth_azure_hf_callback in backend/chainlit/server.py mirrored the generic OAuth callback except that it declared no state argument and never called validate_oauth_state_cookie; the generic route did. It read code and id_token as client form fields and forwarded the raw id_token into config.code.oauth_callback(...). No signature, issuer, audience, expiry or nonce check existed anywhere along that path.
Separately, the nonce afforded no protection even where it was generated: AzureADHybridOAuthProvider.__init__ drew a nonce and stored it on self.authorize_params, but provider instances are built once at module import, so the same nonce was sent on every authorization request for the process lifetime, and it was never compared against the returned token.
Impact
An unauthenticated attacker can obtain a valid session cookie for any identity on the deployment, including administrative users, and then read or act through the authenticated API as that principal without holding any Azure AD credential.
PoC summary
Live poisoning of the callback with an alg=none forged token, against the real app with Azure calls stubbed:
$ POST /auth/oauth/azure-ad-hybrid/callback {code: <code>, id_token: <alg=none forged, upn=victim@example.com>}
-> 302 .../login/callback?success=True Set-Cookie: access_token=eyJ...
$ GET /user (using that cookie)
-> 200 {"identifier":"victim@example.com", ...}
A nonce was observed to be reused across independent logins and never verified. (See attached scan report for the full reproduction harness.)
Fix
oauth_azure_hf_callbacknow declares a requiredstateform field and callsvalidate_oauth_state_cookie(request, state)before processing the callback, matching the generic route.- The
id_tokenis verified before it is trusted or forwarded: signature against the tenant JWKS, plusaud,issandexp. - The OIDC
nonceis now generated per authorization request inoauth_login, bound to the browser via an httponly cookie, and compared against the returnedid_token'snonceclaim (hmac.compare_digest). The nonce is no longer derived from a provider instance constructed once at module import.
Verification
python -m py_compileon the three changed modules.- Harness re-run after the fix: forged
id_tokenwithout state →303 /login?error=oauthSignin, noaccess_tokencookie; nonce differs across independent logins (nonce_reused: false).
Scan
Scan ID: cmuwxf0uj0073lf01nna76pt1 · Finding: cmux90q9j00cvlf01x8qc9wna
⚠️ This is an automated security PR with a framework-level auth change; please review carefully.
Summary by cubic
Hardens the azure-ad-hybrid OAuth callback so it validates the OAuth state and verifies the id_token before issuing a session. Previously the callback accepted a client-supplied id_token without state or token validation, letting an unauthenticated attacker mint a session for any identity.
Security fix
- Requires and validates the
stateform field against the OAuth state cookie, matching the generic OAuth callback. - Verifies the
id_tokensignature against the tenant JWKS and checksaud,iss, andexpbefore trusting it. - Generates a fresh OIDC
nonceper login, bound to the browser via an httponly cookie, and compares it against the returned token'snonceclaim. - Drops the process-wide
noncethat was set once at module import on the provider instance.