Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/sso/id_jag_assertion_capture.py: 28%
37 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Whether the SSO provider the login callback dispatches to can capture an IdP identity assertion.
3An ``oauth2_id_jag`` MCP server spends the ``id_token`` captured at SSO login as its RFC 8693
4subject token. Only the generic OIDC login path reaches a token response the gateway retains one
5from, so a deployment whose SSO runs through Google, Microsoft or SAML never stores an assertion
6and every store-sourced ID-JAG exchange fails for every user, however many times they sign in.
7Neither side can see that alone: the MCP registration knows nothing about SSO and the login knows
8nothing about MCP. This module is the one shared answer both warn from.
9"""
11from __future__ import annotations
13import os
14from enum import Enum
16from typing_extensions import assert_never
18from litellm.proxy.management_endpoints.sso.saml_sso import SAMLAuthHandler
20_GENERIC_OIDC_REMEDY = (
21 "Point SSO at the generic OIDC provider (GENERIC_CLIENT_ID), the one login path whose token "
22 "response the gateway retains an id_token from"
23)
26class ActiveSSOProvider(str, Enum):
27 google = "google"
28 microsoft = "microsoft"
29 generic = "generic"
30 saml = "saml"
31 none = "none"
34def active_sso_provider() -> ActiveSSOProvider:
35 """The provider the SSO callback will dispatch to.
37 Mirrors the callback's precedence rather than reporting everything configured: an environment
38 carrying both GOOGLE_CLIENT_ID and GENERIC_CLIENT_ID runs the Google branch, so it must report
39 Google. Presence is judged the way the callback judges it, so a client id set to the empty
40 string still selects that branch here.
41 """
42 if os.getenv("GOOGLE_CLIENT_ID") is not None:
43 return ActiveSSOProvider.google
44 if os.getenv("MICROSOFT_CLIENT_ID") is not None:
45 return ActiveSSOProvider.microsoft
46 if os.getenv("GENERIC_CLIENT_ID") is not None:
47 return ActiveSSOProvider.generic
48 if SAMLAuthHandler.is_saml_configured():
49 return ActiveSSOProvider.saml
50 return ActiveSSOProvider.none
53def id_jag_assertion_capture_gap() -> str | None:
54 """Why ID-JAG cannot work under the active SSO provider, phrased for an operator reading a log,
55 or ``None`` when that provider does capture an assertion."""
56 provider = active_sso_provider()
57 match provider:
58 case ActiveSSOProvider.generic:
59 return None
60 case ActiveSSOProvider.none:
61 return (
62 "no SSO provider is configured, so no IdP identity assertion is ever captured and "
63 f"ID-JAG credential resolution fails for every user. {_GENERIC_OIDC_REMEDY}"
64 )
65 case ActiveSSOProvider.google | ActiveSSOProvider.microsoft | ActiveSSOProvider.saml:
66 return (
67 f"the active SSO provider ({provider.value}) has no identity-assertion capture path, so no "
68 "IdP id_token is ever stored and ID-JAG credential resolution fails for every user no matter "
69 f"how often they sign in. {_GENERIC_OIDC_REMEDY}"
70 )
71 case _:
72 assert_never(provider)
75def id_jag_assertion_capture_gap_at_startup() -> str | None:
76 """Config load runs before SSO settings stored in the database are reconciled into the process
77 environment, so an unresolved provider at that point is not yet a gap; the SSO callback reports it
78 once a login happens."""
79 if active_sso_provider() is ActiveSSOProvider.none:
80 return None
81 return id_jag_assertion_capture_gap()