fix(oidc): require azp == client_id on multi-audience ID tokens (#385) - #405
Conversation
The verifier only checked 'client_id in aud' when aud was a JSON array. Per OIDC Core 1.0 §3.1.3.7, a token whose aud lists multiple audiences must also contain an azp claim equal to the RP's client_id. Without that check, a token issued by the same IdP for a sibling client that happens to include this client in its audience list was accepted, letting one tenant's tokens authenticate against another tenant's sessions. Fixes #385
📝 WalkthroughWalkthroughThis PR implements OIDC specification compliance by adding multi-audience ID token validation. When an ID token's ChangesMulti-Audience ID Token Validation
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
openrag/components/auth/test_oidc_client.py (1)
256-279: ⚡ Quick winAdd an explicit missing-
azpregression case.Current negative coverage only verifies mismatched
azp. On Line 264’s stated intent, add a variant with multi-audienceaudand noazpclaim to lock the full requirement.Suggested test refinement
- async def test_multi_aud_requires_matching_azp(self, client): + `@pytest.mark.parametrize`( + "extra_claims", + [ + {"aud": [CLIENT_ID, "other-client"]}, # missing azp + {"aud": [CLIENT_ID, "other-client"], "azp": "other-client"}, # mismatched azp + ], + ) + async def test_multi_aud_requires_matching_azp(self, client, extra_claims): _setup_discovery(client._mock_router) _setup_jwks(client._mock_router) nonce = "n-azp" - # Multi-aud token with the wrong (or missing) azp must be rejected - id_token = _sign_jwt( - _id_token_payload(nonce, extra={"aud": [CLIENT_ID, "other-client"], "azp": "other-client"}) - ) + id_token = _sign_jwt(_id_token_payload(nonce, extra=extra_claims))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openrag/components/auth/test_oidc_client.py` around lines 256 - 279, Add a second negative variant to test_multi_aud_requires_matching_azp that covers the missing azp case: create an id_token via _sign_jwt(_id_token_payload(nonce, extra={"aud": [CLIENT_ID, "other-client"]})) (omit "azp"), mock the token endpoint return_value with that token_response as in the existing case, then await client.exchange_code(code="code", code_verifier="v", expected_nonce=nonce) inside a pytest.raises(ValueError, match="multi-aud") to assert the multi-audience-without-azp regression is rejected; reuse the same mocking pattern and variables (client._mock_router.post(...).mock(...), token_response, nonce) used in the existing test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@openrag/components/auth/test_oidc_client.py`:
- Around line 256-279: Add a second negative variant to
test_multi_aud_requires_matching_azp that covers the missing azp case: create an
id_token via _sign_jwt(_id_token_payload(nonce, extra={"aud": [CLIENT_ID,
"other-client"]})) (omit "azp"), mock the token endpoint return_value with that
token_response as in the existing case, then await
client.exchange_code(code="code", code_verifier="v", expected_nonce=nonce)
inside a pytest.raises(ValueError, match="multi-aud") to assert the
multi-audience-without-azp regression is rejected; reuse the same mocking
pattern and variables (client._mock_router.post(...).mock(...), token_response,
nonce) used in the existing test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5bf19cd0-0ec8-4a04-a13a-a86f86d86069
📒 Files selected for processing (2)
openrag/components/auth/test_oidc_client.pyopenrag/services/auth/oidc_client.py
Summary
services/auth/oidc_client.pyonly checkedclient_id in audwhenaudwas a JSON array. Per OIDC Core 1.0 §3.1.3.7, a token whoseaudlists multiple audiences must also contain anazpclaim equal to the RP'sclient_id. Without that check, a token issued by the same IdP for a sibling client that happened to include this client in its audience list was accepted, letting one tenant's tokens authenticate against another tenant's sessions.This PR adds the
azpvalidation branch in the ID token verifier. (The logout-token verifier is intentionally left alone for now: back-channel logout already requires a matching session id and is a different threat model — happy to apply the same change there if reviewers prefer.)Test plan
azp == client_idvalidatesazpis rejectedFixes #385
Summary by CodeRabbit
Bug Fixes
Tests