stacklok / stacklok/toolhive

Implement JWT-SVID OAuth client authentication (spiffe_jwt)

Open
#6,203 1 comment 0 reactions 1 assignee View on GitHub

@jhrozek is already working on this.

Since Aug 14, 2026.

authentication authorization enhancement go
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_assertion with
    client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-spiffe.
  • Validate JWT-SVID signatures using only the jwt-svid keys of the configured
    trust-domain bundle from sub-issue 2.
  • Extract the SPIFFE ID from sub and resolve it through the shared
    client-association policy from sub-issue 1.
  • Enforce assertion size, cardinality, algorithm, expiry and clock-skew limits.
  • Advertise spiffe_jwt only when the method is fully configured and
    operational.

Acceptance criteria

Assertion acceptance:

  • client_assertion_type matches
    urn:ietf:params:oauth:client-assertion-type:jwt-spiffe exactly.
  • The request contains exactly one JWT-SVID assertion.
  • sub is a valid SPIFFE ID associated with the requested OAuth client.
  • exp is present and valid.
  • aud contains the ToolHive authorization-server issuer as its sole value.
  • The signature validates using only permitted jwt-svid keys 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_type for
    RFC 7523 urn: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.Resolve from 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.