mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-06 07:51:53 +00:00
feat(dashboard-auth-nous): surface token iss/aud in verification-failure error
When jwt.decode raises InvalidTokenError, decode the token a second time without signature verification (safe — we never trust the values, just display them) and append the actual iss/aud claims plus our configured expected values to the error message. Lets operators see config drift between HERMES_DASHBOARD_PORTAL_URL / HERMES_DASHBOARD_OAUTH_CLIENT_ID and what Portal is actually emitting without having to hand-decode the JWT from the browser cookie.
This commit is contained in:
parent
42729775db
commit
a498485631
2 changed files with 34 additions and 1 deletions
|
|
@ -356,8 +356,28 @@ class NousDashboardAuthProvider(DashboardAuthProvider):
|
|||
# verify_session() catches this and returns None per protocol.
|
||||
raise InvalidCodeError(f"access token expired: {exc}") from exc
|
||||
except jwt.InvalidTokenError as exc:
|
||||
# Surface the actual claim values that failed verification so
|
||||
# operators don't have to dig into the JWT to debug config drift
|
||||
# between HERMES_DASHBOARD_PORTAL_URL / HERMES_DASHBOARD_OAUTH_CLIENT_ID
|
||||
# and what Portal is actually emitting. Decoding without verification
|
||||
# is safe here: we've already failed to verify, and we never trust
|
||||
# these values — they're surfaced for diagnostics only.
|
||||
details = ""
|
||||
try:
|
||||
unverified = jwt.decode(
|
||||
access_token,
|
||||
options={"verify_signature": False, "verify_exp": False},
|
||||
)
|
||||
details = (
|
||||
f" [token iss={unverified.get('iss')!r} "
|
||||
f"aud={unverified.get('aud')!r}; "
|
||||
f"expected iss={self._portal_url!r} "
|
||||
f"aud={self._client_id!r}]"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
raise ProviderError(
|
||||
f"access token verification failed: {exc}"
|
||||
f"access token verification failed: {exc}{details}"
|
||||
) from exc
|
||||
|
||||
self._check_agent_instance_id(claims)
|
||||
|
|
|
|||
|
|
@ -504,6 +504,19 @@ class TestVerifySession:
|
|||
with pytest.raises(ProviderError, match="verification failed"):
|
||||
provider.verify_session(access_token=token)
|
||||
|
||||
def test_verification_failure_message_surfaces_token_claims(
|
||||
self, provider, rsa_keypair
|
||||
):
|
||||
"""Operators need to see the actual iss/aud the token carries to debug
|
||||
config drift between HERMES_DASHBOARD_PORTAL_URL/CLIENT_ID and Portal."""
|
||||
token = _mint_token(rsa_keypair, iss="https://evil.example")
|
||||
with pytest.raises(ProviderError) as excinfo:
|
||||
provider.verify_session(access_token=token)
|
||||
msg = str(excinfo.value)
|
||||
# Both the observed (token) and expected (configured) values appear.
|
||||
assert "'https://evil.example'" in msg
|
||||
assert "'https://portal.example.com'" in msg # configured portal URL
|
||||
|
||||
def test_missing_sub_raises(self, provider, rsa_keypair):
|
||||
# PyJWT's "require" set includes sub, so this surfaces as
|
||||
# InvalidTokenError → ProviderError before we ever touch _session_from_claims.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue