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

1"""Whether the SSO provider the login callback dispatches to can capture an IdP identity assertion. 

2 

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""" 

10 

11from __future__ import annotations 

12 

13import os 

14from enum import Enum 

15 

16from typing_extensions import assert_never 

17 

18from litellm.proxy.management_endpoints.sso.saml_sso import SAMLAuthHandler 

19 

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) 

24 

25 

26class ActiveSSOProvider(str, Enum): 

27 google = "google" 

28 microsoft = "microsoft" 

29 generic = "generic" 

30 saml = "saml" 

31 none = "none" 

32 

33 

34def active_sso_provider() -> ActiveSSOProvider: 

35 """The provider the SSO callback will dispatch to. 

36 

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 

51 

52 

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) 

73 

74 

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()