fix(auth): validate OAuth state and verify id_token in azure-ad-hybrid callback

FA
1mo ago
+106-3

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

  1. oauth_azure_hf_callback now declares a required state form field and calls validate_oauth_state_cookie(request, state) before processing the callback, matching the generic route.
  2. The id_token is verified before it is trusted or forwarded: signature against the tenant JWKS, plus aud, iss and exp.
  3. The OIDC nonce is now generated per authorization request in oauth_login, bound to the browser via an httponly cookie, and compared against the returned id_token's nonce claim (hmac.compare_digest). The nonce is no longer derived from a provider instance constructed once at module import.

Verification

  • python -m py_compile on the three changed modules.
  • Harness re-run after the fix: forged id_token without state → 303 /login?error=oauthSignin, no access_token cookie; 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 state form field against the OAuth state cookie, matching the generic OAuth callback.
  • Verifies the id_token signature against the tenant JWKS and checks aud, iss, and exp before trusting it.
  • Generates a fresh OIDC nonce per login, bound to the browser via an httponly cookie, and compares it against the returned token's nonce claim.
  • Drops the process-wide nonce that was set once at module import on the provider instance.