Files
aiturk-hermes-ide/tests/plugins/dashboard_auth/test_opaque_bearer_not_unreachable.py
T

157 lines
6.1 KiB
Python

"""#94558 — a non-JWT bearer must not be reported as "Auth provider unreachable".
Hosted agents answered every opaque/peer bearer on the gated API with a fast
HTTP 503 ``{"detail": "Auth provider 'nous' unreachable"}`` while Portal was
perfectly healthy: ``NousDashboardAuthProvider._verify_jwt`` folded *every*
``PyJWKClient`` failure — including ``DecodeError('Not enough segments')`` for
a token that is not a JWT at all — into ``ProviderError``. Only a transport
failure fetching the JWKS is "unreachable"; anything else means "not my
token" (``verify_session`` -> None -> 401 / next provider).
Real ``NousDashboardAuthProvider`` + real ``SelfHostedOIDCProvider`` JWKS path,
a real local HTTP JWKS server (reachable case) or a closed port (unreachable),
and the real gated web_server app for the HTTP-level assertion.
"""
from __future__ import annotations
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import jwt
import pytest
from starlette.testclient import TestClient
from hermes_cli import web_server
from hermes_cli.dashboard_auth import (
InvalidCodeError,
ProviderError,
classify_jwks_lookup_error,
clear_providers,
register_provider,
)
from hermes_cli.dashboard_auth.cookies import SESSION_AT_COOKIE
import plugins.dashboard_auth.nous as nous_plugin
OPAQUE_PEER_KEY = "hk_live_opaque_peer_key_0123456789abcdef"
# Well-formed RS256 JWT header with an unknown kid, bogus payload/signature.
FOREIGN_KID_JWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6Inp6eiJ9.e30.sig"
@pytest.fixture(scope="module")
def empty_jwks_server():
"""A reachable JWKS endpoint that knows no keys."""
class _H(BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802
self.send_response(200)
self.send_header("content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({"keys": []}).encode())
def log_message(self, *a): # silence
pass
srv = HTTPServer(("127.0.0.1", 0), _H)
t = threading.Thread(target=srv.serve_forever, daemon=True)
t.start()
yield f"http://127.0.0.1:{srv.server_address[1]}"
srv.shutdown()
def _nous(portal_url: str) -> nous_plugin.NousDashboardAuthProvider:
return nous_plugin.NousDashboardAuthProvider(client_id="agent:test-instance", portal_url=portal_url)
# ── classifier ────────────────────────────────────────────────────────────
def test_classifier_maps_transport_failure_to_provider_error():
exc = jwt.PyJWKClientConnectionError("Fail to fetch data from the url")
assert isinstance(classify_jwks_lookup_error(exc), ProviderError)
@pytest.mark.parametrize(
"exc",
[
jwt.DecodeError("Not enough segments"),
jwt.PyJWKSetError("The JWK Set did not contain any keys"),
jwt.InvalidTokenError("bad"),
],
)
def test_classifier_maps_unverifiable_token_to_invalid_code(exc):
assert isinstance(classify_jwks_lookup_error(exc), InvalidCodeError)
def test_classifier_keeps_bare_jwk_client_error_as_provider_fault():
assert isinstance(classify_jwks_lookup_error(jwt.PyJWKClientError("weird JWKS shape")), ProviderError)
# ── Nous provider ─────────────────────────────────────────────────────────
def test_opaque_bearer_with_healthy_portal_is_not_unreachable(empty_jwks_server):
provider = _nous(empty_jwks_server)
assert provider.verify_session(access_token=OPAQUE_PEER_KEY) is None
def test_foreign_kid_jwt_with_healthy_portal_is_not_unreachable(empty_jwks_server):
provider = _nous(empty_jwks_server)
assert provider.verify_session(access_token=FOREIGN_KID_JWT) is None
def test_real_jwt_with_unreachable_portal_still_raises_provider_error():
provider = _nous("http://127.0.0.1:9") # discard port: connection refused
with pytest.raises(ProviderError):
provider.verify_session(access_token=FOREIGN_KID_JWT)
def test_opaque_bearer_with_unreachable_portal_is_still_just_not_ours():
"""No network call is even needed to know an opaque string is not our JWT."""
provider = _nous("http://127.0.0.1:9")
assert provider.verify_session(access_token=OPAQUE_PEER_KEY) is None
# ── self-hosted OIDC provider (sibling site of the same hunk) ──────────────
def test_self_hosted_provider_shares_the_classification(empty_jwks_server, monkeypatch):
import plugins.dashboard_auth.self_hosted as sh
provider = object.__new__(sh.SelfHostedOIDCProvider)
provider._jwks_client = None
provider._client_id = "hermes"
monkeypatch.setattr(
provider, "_get_discovery",
lambda: {"jwks_uri": f"{empty_jwks_server}/jwks", "issuer": empty_jwks_server},
)
with pytest.raises(InvalidCodeError):
provider._verify_id_token(OPAQUE_PEER_KEY)
# ── HTTP level: the gated API answers 401, not 503 ────────────────────────
@pytest.fixture
def _gated_nous(empty_jwks_server):
clear_providers()
prev = {k: getattr(web_server.app.state, k, None) for k in ("bound_host", "bound_port", "auth_required")}
web_server.app.state.bound_host = "agent.example.test"
web_server.app.state.bound_port = 443
web_server.app.state.auth_required = True
register_provider(_nous(empty_jwks_server))
yield TestClient(web_server.app, base_url="https://agent.example.test")
clear_providers()
for k, v in prev.items():
setattr(web_server.app.state, k, v)
def test_gated_api_rejects_opaque_bearer_with_401_not_503(_gated_nous):
r = _gated_nous.get("/api/auth/me", headers={"Authorization": f"Bearer {OPAQUE_PEER_KEY}"})
assert r.status_code != 503, r.text
assert r.status_code == 401
assert "unreachable" not in r.text.lower()
def test_gated_api_rejects_opaque_cookie_with_401_not_503(_gated_nous):
_gated_nous.cookies.set(SESSION_AT_COOKIE, OPAQUE_PEER_KEY)
r = _gated_nous.get("/api/auth/me")
assert r.status_code != 503, r.text
assert "unreachable" not in r.text.lower()