Implement JWT-SVID OAuth client authentication (spiffe_jwt)
@jhrozek is already working on this.
Since Aug 14, 2026.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Description
Implement the spiffe_jwt client-authentication method: a workload presents a
JWT-SVID as an OAuth client assertion, and the authorization server
authenticates the client from the validated assertion.
This must work independently of the TLS peer-certificate path. Deployments
that terminate TLS at an ingress cannot present a client certificate to the
authorization server at all, so JWT-SVID is not a convenience alternative to
spiffe_x509 — it is the only SPIFFE option in a large class of real
deployments.
The dominant risk here is type confusion. A JWT-SVID, an OIDC ID token and an
ordinary access token are all signed JWTs, and the same IdP may sign several of
them. Accepting the wrong one as a client assertion would let any bearer of a
routine token authenticate as the workload.
Scope
- Accept exactly one
client_assertionwith
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe. - Validate JWT-SVID signatures using only the
jwt-svidkeys of the configured
trust-domain bundle from sub-issue 2. - Extract the SPIFFE ID from
suband resolve it through the shared
client-association policy from sub-issue 1. - Enforce assertion size, cardinality, algorithm, expiry and clock-skew limits.
- Advertise
spiffe_jwtonly when the method is fully configured and
operational.
Acceptance criteria
Assertion acceptance:
client_assertion_typematches
urn:ietf:params:oauth:client-assertion-type:jwt-spiffeexactly.- The request contains exactly one JWT-SVID assertion.
subis a valid SPIFFE ID associated with the requested OAuth client.expis present and valid.audcontains the ToolHive authorization-server issuer as its sole value.- The signature validates using only permitted
jwt-svidkeys from the
associated trust-domain bundle.
Rejection:
- Ordinary OIDC access tokens, ID tokens and generic signed JWTs are rejected.
- Wrong audiences, trust domains, keys, algorithms and client associations are
rejected. - A malformed or unauthorized assertion cannot fall back to public or
secret-based client authentication.
Hygiene:
- Assertions and their claims are never written to logs.
Reuse
Signature verification and key resolution come from sub-issue 2's bundle
registry, restricted to the bundle's jwt-svid keys. Use
jwtsvid.ParseAndValidate(token, jwtBundleSource, audience) from go-spiffe/v2
— it consumes the jwtbundle.Source #6201 produces. This issue adds the
SVID-specific claim rules and the type-confusion defences, not a validation
stack.
Also lift pkg/authserver/spiffe/context.go from the spiffee-authserver
branch (26 lines). Identity lands in a context key rather than a
certificate-shaped struct, which is what lets this issue produce the same
principal as #6202 without touching downstream code.
Correction to the original text: this issue previously named
pkg/oauthproto/jwks.go as the source of signature verification. That is wrong
twice over. SPIFFE JWT authorities come from the trust bundle, not from a JWKS
URI, so jwtsvid.ParseAndValidate makes no HTTP request and no fetcher is
involved. And that file is an unpushed spike artifact that should not be adopted
— see #6319 for the separate JWKS consolidation work.
Dependencies
- Sub-issue 1 (identity, association and configuration model)
- Sub-issue 2 (trust-domain bundle management) — specifically its
jwt-svid
key restriction
Can proceed in parallel with the X.509-SVID sub-issue once both foundations
exist.
Coordination with the sibling credential type
#6202 and #6203 replace the same fosite hook: a single
ClientAuthenticationStrategy
(func(context.Context, *http.Request, url.Values) (fosite.Client, error)).
#6202 dispatches on a SPIFFE ID placed in the request context by the mTLS
listener; #6203 dispatches on client_assertion_type in the request form.
That dispatch is introduced once, separately, as a no-behaviour-change change
before either issue starts — both SPIFFE arms stubbed, everything else falling
through to fosite's default strategy. This issue owns one arm only and must
not modify the dispatch itself. A needed change there goes back to the shared
change rather than being patched in place.
Two things to know when filling in an arm:
- Fosite's default strategy already dispatches on
client_assertion_typefor
RFC 7523urn:ietf:params:oauth:client-assertion-type:jwt-bearer. The SPIFFE
assertion type is different and must be matched exactly, or existing
private-key-JWT client authentication breaks. - Both arms resolve through
SPIFFEAssociationRegistry.Resolvefrom sub-issue
1, which already fails closed on unknown association, wrong client ID, and
disabled method. Do not add a parallel lookup — that registry is what makes
the "equivalent authorization outcome" guarantee enforceable rather than
merely tested.
Related
- Part of the SPIFFE client-authentication epic
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.