diff --git a/.github/demo-assets/TEST_RESULTS.md b/.github/demo-assets/TEST_RESULTS.md new file mode 100644 index 00000000000..9e34862f9e4 --- /dev/null +++ b/.github/demo-assets/TEST_RESULTS.md @@ -0,0 +1,72 @@ +## JWT Auth Fixes — Integration Test Results + +All three fixes verified end-to-end against a live LiteLLM proxy with mock JWKS/OIDC server, fake LLM backend, and RSA-signed JWTs. + +### Setup + +| Component | Port | Description | +|---|---|---| +| Mock JWKS + OIDC | :19900 | Serves `/.well-known/openid-configuration` → `jwks_uri` | +| Fake LLM | :19901 | Returns canned chat completions | +| LiteLLM Proxy | :19902 | JWT auth enabled, `premium_user` patched, `team_id_upsert: true` | + +--- + +### Fix 1: OIDC Discovery URL Resolution + +`JWT_PUBLIC_KEY_URL` set to `.well-known/openid-configuration` (not a direct JWKS URL). + +| Test | Token | Expected | Got | +|---|---|---|---| +| 1a | Valid JWT (RSA-signed, correct key) | **200** | ✅ **200** — chat completion returned | +| 1b | Tampered JWT (last 5 chars replaced) | **401** | ✅ **401** — "Signature verification failed" | + +The proxy correctly fetches the OIDC discovery doc, resolves `jwks_uri`, and validates the JWT signature. + +--- + +### Fix 2: Roles as Array + +Config: `team_id_jwt_field: "roles"` — JWT `roles` claim is a JSON array. + +| Test | Token Claims | Expected | Got | +|---|---|---|---| +| 2a | `"roles": ["team-beta", "team-gamma"]` | **200** (first element used) | ✅ **200** — `team-beta` extracted, completion returned | +| 2b | `"roles": []` | **401** | ✅ **401** — "No team found in token" | + +![Fix 1+2 results](https://raw.githubusercontent.com/BerriAI/litellm/68c86d22396ea4dce14e646dc9298c7b8e5f18ee/.github/demo-assets/fix1_fix2_results.webp) + +--- + +### Fix 3: Dot-notation Error Hint + +Config: `team_id_jwt_field: "roles.0"` — common Azure AD misconfiguration. + +| Test | Token Claims | Expected | Got | +|---|---|---|---| +| 3 | `"roles": ["team-alpha"]` | **401** with hint | ✅ **401** — hint says: *"Use 'roles' instead — LiteLLM automatically uses the first element when the field value is a list."* | + +![Fix 3 results](https://raw.githubusercontent.com/BerriAI/litellm/68c86d22396ea4dce14e646dc9298c7b8e5f18ee/.github/demo-assets/fix3_results.webp) + +--- + +### Unit Tests + +13/13 new tests pass: + +``` +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_resolve_jwks_url_passthrough_for_direct_jwks_url PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_resolve_jwks_url_resolves_oidc_discovery_document PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_resolve_jwks_url_caches_resolved_jwks_uri PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_resolve_jwks_url_raises_if_no_jwks_uri_in_discovery_doc PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_get_team_id_returns_first_element_when_roles_is_list PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_get_team_id_returns_first_element_from_multi_value_roles_list PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_get_team_id_returns_default_when_roles_list_is_empty PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_get_team_id_still_works_with_string_value PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_get_team_id_list_result_is_hashable PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_find_and_validate_specific_team_id_hints_bracket_notation PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_find_and_validate_specific_team_id_hints_bracket_index_notation PASSED +tests/test_litellm/proxy/auth/test_handle_jwt.py::test_find_and_validate_specific_team_id_no_hint_for_valid_field PASSED +``` + +All 13 passed ✅