Judge a JWT by its issuer, not its shape - #41
Merged
Merged
Conversation
The JWT-shape refusal exists to keep Google ID tokens and service account tokens from being sent to a service they were never meant for, but it read every three-segment token as one of Google's. OAuth providers issue JWT-shaped access tokens of their own - Zendesk's carry only exp and name no issuer - so the shape convicts nothing. What a JWT names is what is judged now. One that names an issuer must name the connection's own service (its hosts or OAuth endpoints); one that names none says as little as an opaque token and is judged by elimination like one; one that cannot be read is refused rather than guessed about. Google's credentials still fall out without a list of Google's issuers: ID tokens and service account tokens always name theirs, which is never the connection's own. 🤖 Generated with Claude Code
json.loads recurses per nesting level, so a payload of nothing but
brackets escaped the ValueError net as a RecursionError and blew up
the tool call instead of being refused. The claims arrive inside an
unvetted token; however they defeat the reader, the answer is
refusal, not a traceback.
A null issuer slipped the other way: claims.get("iss") read it as no
issuer at all, and the token went on to be judged by elimination.
RFC 7519 wants a StringOrURI wherever iss is present, so a
present-but-null issuer is as malformed as a numeric one and is
refused the same.
🤖 Generated with Claude Code
A prefixless connection declares no shapes, so a JWT refused for its issuer was logged as matching none of them - true, but a mystery to whoever reads the warning, and the issuer is exactly what needs diagnosing when a service's tokens change under a connection. The description now names what refused the token: claims that cannot be read, or an issuer that does not name this service. The token itself still never reaches the log. 🤖 Generated with Claude Code
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Exchanging an authorization code against a Zendesk tenant returns a
JWT-shaped access token: three dot-separated segments, a base64url JSON
header, and claims that carry only
exp— no issuer.accepts_tokenrefused every JWT-shaped token for a prefixless connection, so a Zendesk
authorization could never produce a usable token.
Why shape cannot convict
The refusal exists so that a Google ID token or service account token
handed over by mistake is never sent to an external service. Both are
JWTs — but so are many services' access tokens. "It is a JWT" does not
imply "it is Google's", and the two kinds tell themselves apart by
something no shape check can see: the issuer the claims name.
What is judged now
For a connection without token prefixes (a declared prefix still decides
on its own):
the connection's own service — a host the token may travel to, or the
host of an OAuth endpoint, since the issuer of a service's tokens is
its authorization server, which may live beside the API rather than on
it.
opaque token, so it is judged like one: by elimination. Zendesk's
tokens are shaped this way.
about, exactly as before.
Google's credentials fall out of the first rule without a list of
Google's issuers: ID tokens name
accounts.google.com, service accounttokens name the account's own address, and neither is ever the
connection's own host. The signature is deliberately not checked —
nothing here authenticates anyone. The claims are read the way a token
prefix is, to tell whose token this is before it travels; a forged
issuer gains nothing but a request its token cannot answer.
What holds it
every connection against Google-issued JWTs (ID-token and
service-account shaped) instead, since an issuerless JWT is exactly
what a prefixless connection may now accept.
zendesk.yamlrecords the observed shape as anacceptsexample;freeeandnotion-mcpkeep refusing Google-issued JWTs in theirs.urlsplitcannot read (
https://[) is refused instead of raising.