Skip to content

feat(go-jwks-multi): add multi-issuer JWT validation example#17

Merged
appleboy merged 12 commits into
mainfrom
feat/go-jwks-multi-example
Apr 25, 2026
Merged

feat(go-jwks-multi): add multi-issuer JWT validation example#17
appleboy merged 12 commits into
mainfrom
feat/go-jwks-multi-example

Conversation

@appleboy
Copy link
Copy Markdown
Member

@appleboy appleboy commented Apr 25, 2026

Summary

  • Add a new examples/go-jwks-multi/ resource server that accepts AuthGate-issued tokens from N trusted issuers, dispatching by iss claim to per-issuer offline JWKS verifiers.
  • Enforce per-route allowlists for three custom claims — tenant, service_account, project — via an accessRule struct attached to each route. Empty allowlist = "don't check"; non-empty = AND-combined and fail-closed on missing claim.
  • Add an optional ISSUER_TENANTS cross-tenant defense map that pins each issuer to the set of tenant codes 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.
  • Add 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 /sign endpoint 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.
  • Update the repo root README.md table + section to reference the new example.

Trust model

The middleware reads iss from the unverified payload only to pick which verifier to use; the chosen verifier then authoritatively re-checks iss, validates the RS256 signature against that issuer's cached JWKS, and enforces aud / exp / nbf. The ISSUER_TENANTS and per-route accessRule checks run only after Verify() succeeds.

Test plan

  • `go build ./...` + `go vet ./...` clean for both `go-jwks-multi` and `go-jwks-multi/testissuer`
  • End-to-end smoke test against the testissuer:
    • Happy path: token from auth-a with `tenant=oa` → 200 on `/api/profile`
    • Cross-tenant attack: auth-a signs `tenant=swrd` (owned by auth-b) → 401 with `issuer×tenant reject` in server log
    • Route policy reject: auth-b's valid `tenant=swrd` token hits `/api/data` (allows only `oa,hwrd`) → 401 with `policy reject` in server log
    • Listener startup ordering: the env-block banner only prints after both sockets are bound; port-in-use causes immediate fail-fast before any banner
  • Manual review against your real AuthGate(s) using `../go-jwks/get-token.sh` as documented in the README

🤖 Generated with Claude Code

- 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>
Copilot AI review requested due to automatic review settings April 25, 2026 04:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 optional ISSUER_TENANTS issuer→tenant pinning.
  • Added go-jwks-multi/testissuer/ helper that runs two local fake issuers with OIDC discovery + JWKS + /sign for end-to-end testing.
  • Updated the repo root README to reference the new go-jwks-multi example.

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.

Comment thread go-jwks-multi/testissuer/main.go Outdated
Comment thread go-jwks-multi/testissuer/main.go Outdated
Comment thread go-jwks-multi/testissuer/README.md
Comment thread go-jwks-multi/testissuer/README.md
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/testissuer/main.go
Comment thread go-jwks-multi/main.go Outdated
Comment thread go-jwks-multi/README.md Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go Outdated
Comment thread go-jwks-multi/testissuer/main.go Outdated
Comment thread go-jwks-multi/main.go Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/README.md Outdated
Comment thread go-jwks-multi/README.md Outdated
Comment thread go-jwks-multi/testissuer/README.md Outdated
Comment thread go-jwks-multi/testissuer/main.go Outdated
Comment thread go-jwks-multi/main.go
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/README.md Outdated
Comment thread go-jwks-multi/main.go
Comment thread go-jwks-multi/testissuer/main.go Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go
Comment thread go-jwks-multi/testissuer/main.go Outdated
Comment thread README.md Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go Outdated
Comment thread go-jwks-multi/testissuer/main.go Outdated
- 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>
@appleboy appleboy requested a review from Copilot April 25, 2026 05:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread README.md Outdated
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go
Comment thread go-jwks-multi/main.go
@appleboy appleboy requested a review from Copilot April 25, 2026 06:10
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread go-jwks-multi/main.go
Comment thread go-jwks-multi/main.go
Comment thread go-jwks-multi/testissuer/README.md
- 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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@appleboy appleboy merged commit f519e10 into main Apr 25, 2026
8 checks passed
@appleboy appleboy deleted the feat/go-jwks-multi-example branch April 25, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants