feat(go-jwks-multi): add multi-issuer JWT validation example#17
Conversation
- Add resource server that accepts tokens from N trusted AuthGate issuers via per-iss verifier dispatch - Enforce per-route allowlists for tenant, service_account, and project custom claims - Add ISSUER_TENANTS map for cross-tenant defense against compromised issuers signing for other tenants - Add testissuer sub-tool that runs two local fake AuthGates and mints JWTs from query params for end-to-end testing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Go example module demonstrating a resource server that validates JWTs offline via JWKS while trusting multiple issuers, including optional cross-tenant defenses and a local multi-issuer test tool.
Changes:
- Added
go-jwks-multi/multi-issuer resource server with per-route scope + custom-claim allowlists and optionalISSUER_TENANTSissuer→tenant pinning. - Added
go-jwks-multi/testissuer/helper that runs two local fake issuers with OIDC discovery + JWKS +/signfor end-to-end testing. - Updated the repo root README to reference the new
go-jwks-multiexample.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| go-jwks-multi/main.go | Implements multi-issuer verifier routing by iss, per-route access rules, and optional issuer→tenant enforcement. |
| go-jwks-multi/README.md | Documents usage, threat model, configuration, and testing flows (including testissuer). |
| go-jwks-multi/.env.example | Provides a copy/paste environment template for the new example. |
| go-jwks-multi/testissuer/main.go | Implements two local fake issuers (discovery, JWKS, token minting) for exercising the resource server. |
| go-jwks-multi/testissuer/README.md | Documents how to run the fake issuers and test scenarios. |
| go-jwks-multi/go.mod | Introduces the new module and its dependencies. |
| go-jwks-multi/go.sum | Adds dependency checksums for the new module. |
| README.md | Adds go-jwks-multi to the quick-reference table and a short section describing it. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Bind testissuer listeners to 127.0.0.1 instead of all interfaces, enforcing the documented "test tool, never expose it" boundary - Use UnixNano + 8 random bytes for kid to avoid collisions on rapid restarts in the same second - Correct cd path in testissuer README to cd go-jwks-multi (this repo is the examples repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Make testissuer iss match the loopback bind by using http://127.0.0.1:port everywhere; localhost can resolve to ::1 on IPv6-first hosts and the listener is IPv4-only, which would break OIDC discovery and iss strict-equality - Replace "probable" with "probeable"/"inferable" in two comments where the security rationale needed the discoverability sense, not the likelihood sense Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Deduplicate verification-failure logs in main.go: untrusted-issuer and cross-tenant rejection paths now return errors carrying iss/tenant/allowed, and middleware is the single place that logs token verification failures - Make testissuer fail loudly when an HTTP server stops unexpectedly: a half-up two-issuer fixture would silently break test scenarios that depend on the missing one Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Reject overlapping tenants in ISSUER_TENANTS at startup; if the same tenant code is listed under more than one issuer, the cross-tenant defense silently degrades to "any of these issuers can sign for it", so fail loudly with a message naming both issuers and the duplicate tenant - Sync README and inline comment log examples with the actual deduplicated middleware output (token verification failed: ...) instead of the pre-dedup wording Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Pass loop variables explicitly into the discovery and serve goroutines so the example reads correctly when copied into a module on a pre-1.22 go directive (Go 1.22+ already makes the implicit capture safe) - Align extraClaims struct field spacing in the README so the snippet matches the source Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Drop the duplicated "tenant `tenant`" leftover from the domain → tenant rename in the file header - Sync testissuer header comment to use `sa` (the actual query param) instead of `service_account` (the resulting claim name) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Switch sub/iss/aud/scope and other claim-derived log fields from %s to %q so newlines and other control characters in attacker-controlled token fields cannot smuggle fake log lines and stay grep-friendly Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Switch strings.Split to strings.SplitN with a cap of 4 so an Authorization header packed with dots cannot allocate hundreds of thousands of substrings before the existing len==3 check rejects it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Add spaces around the column separators on the new row to match the surrounding rows; some markdown renderers drop the cell boundary otherwise Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1c90c18 to
6bde6b5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Set MaxHeaderBytes to 8 KiB on the resource server so the Authorization header (and therefore the JWT payload that unverifiedIssuer base64-decodes before any signature work) can no longer be coerced into large allocations - Sort the canonical issuer list before formatting it into the ISSUER_TENANTS startup error so the message is deterministic and copy-pasteable Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Use tok.Issuer (post-Verify) instead of the pre-verification iss for the cross-tenant lookup so the trust boundary is self-evident - Distinguish same-issuer tenant duplicates (oa,oa) from true cross-issuer overlaps in the ISSUER_TENANTS startup error - Make the get-token.sh helper the primary recipe for decoding JWTs in testissuer README and provide a working alphabet-translating one-liner for the helper-less path Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
examples/go-jwks-multi/resource server that accepts AuthGate-issued tokens from N trusted issuers, dispatching byissclaim to per-issuer offline JWKS verifiers.tenant,service_account,project— via anaccessRulestruct attached to each route. Empty allowlist = "don't check"; non-empty = AND-combined and fail-closed on missing claim.ISSUER_TENANTScross-tenant defense map that pins each issuer to the set oftenantcodes it is permitted to sign for. Stops a compromised issuer A from minting tokens that claim a tenant owned by issuer B — relevant when tenant identifiers are short codes (oa,hwrd,swrd,cdomain) with no DNS-style trust boundary.examples/go-jwks-multi/testissuer/sub-tool that runs two local fake AuthGate instances (auth-a on :9001, auth-b on :9002), each with an ephemeral RSA-2048 keypair, OIDC discovery, JWKS, and a/signendpoint that mints JWTs from query params. Lets you exercise every code path (happy path, untrusted issuer, cross-tenant attack, route policy reject, insufficient scope, missing claim, expired token) without standing up real AuthGates.README.mdtable + section to reference the new example.Trust model
The middleware reads
issfrom the unverified payload only to pick which verifier to use; the chosen verifier then authoritatively re-checksiss, validates the RS256 signature against that issuer's cached JWKS, and enforcesaud/exp/nbf. TheISSUER_TENANTSand per-routeaccessRulechecks run only afterVerify()succeeds.Test plan
🤖 Generated with Claude Code