diff --git a/openrag/components/auth/test_oidc_client.py b/openrag/components/auth/test_oidc_client.py index 8a2e79198..1b45aa2d0 100644 --- a/openrag/components/auth/test_oidc_client.py +++ b/openrag/components/auth/test_oidc_client.py @@ -253,6 +253,52 @@ async def test_nonce_mismatch_raises(self, client): with pytest.raises(ValueError, match="nonce"): await client.exchange_code(code="code", code_verifier="v", expected_nonce="wrong-nonce") + # OIDC Core 1.0 §3.1.3.7: when aud lists multiple audiences, azp MUST + # be present and equal to client_id. Regression for #385. + @pytest.mark.asyncio + async def test_multi_aud_requires_matching_azp(self, client): + _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"}) + ) + token_response = { + "id_token": id_token, + "access_token": "at", + "expires_in": 300, + "token_type": "Bearer", + } + client._mock_router.post(f"{ISSUER}/protocol/openid-connect/token").mock( + return_value=httpx.Response(200, json=token_response) + ) + + with pytest.raises(ValueError, match="multi-aud"): + await client.exchange_code(code="code", code_verifier="v", expected_nonce=nonce) + + @pytest.mark.asyncio + async def test_multi_aud_with_correct_azp_passes(self, client): + _setup_discovery(client._mock_router) + _setup_jwks(client._mock_router) + + nonce = "n-azp-ok" + id_token = _sign_jwt(_id_token_payload(nonce, extra={"aud": [CLIENT_ID, "other-client"], "azp": CLIENT_ID})) + token_response = { + "id_token": id_token, + "access_token": "at", + "expires_in": 300, + "token_type": "Bearer", + } + client._mock_router.post(f"{ISSUER}/protocol/openid-connect/token").mock( + return_value=httpx.Response(200, json=token_response) + ) + + bundle = await client.exchange_code(code="code", code_verifier="v", expected_nonce=nonce) + assert bundle.claims["aud"] == [CLIENT_ID, "other-client"] + assert bundle.claims["azp"] == CLIENT_ID + # --------------------------------------------------------------------------- # Token refresh diff --git a/openrag/services/auth/oidc_client.py b/openrag/services/auth/oidc_client.py index 30db6850b..0a7604a1c 100644 --- a/openrag/services/auth/oidc_client.py +++ b/openrag/services/auth/oidc_client.py @@ -286,6 +286,12 @@ async def _verify_id_token(self, token: str, *, expected_nonce: str | None) -> d if isinstance(aud, list): if self.client_id not in aud: raise ValueError(f"ID token aud {aud!r} does not contain client_id {self.client_id!r}") + # OIDC Core 1.0 §3.1.3.7: when aud lists multiple audiences, azp + # MUST be present and equal to client_id. + if len(aud) > 1: + azp = decoded.get("azp") + if azp != self.client_id: + raise ValueError(f"ID token has multi-aud {aud!r} but azp {azp!r} != client_id {self.client_id!r}") elif aud != self.client_id: raise ValueError(f"ID token aud {aud!r} != client_id {self.client_id!r}")