Skip to content

feat(jwksauth): add UID server-attested claim#29

Merged
appleboy merged 2 commits into
mainfrom
feat/jwksauth-uid-claim
May 9, 2026
Merged

feat(jwksauth): add UID server-attested claim#29
appleboy merged 2 commits into
mainfrom
feat/jwksauth-uid-claim

Conversation

@appleboy
Copy link
Copy Markdown
Member

@appleboy appleboy commented May 9, 2026

Summary

Adds a UID string field to jwksauth.Claims that carries the username from AuthGate-issued tokens under the <prefix>_uid JWT key. UID is populated by user-bearing flows (Authorization Code + PKCE, Device Authorization Grant); Client Credentials tokens have no user, so Claims.UID is the empty string. Threaded through the existing claimKeys / newClaimKeys / newTokenInfo plumbing alongside Domain / Project / ServiceAccount with the same hard-cutover prefix semantics and lazy Extras allocation.

AI Authorship

  • No AI was used in this PR
  • AI was used. Details:
    • Tool / model: Claude Opus 4.7 (1M context) via Claude Code, this session
    • AI-authored files: all 5 — jwksauth/claims.go, jwksauth/claims_prefix_test.go, jwksauth/middleware_test.go, jwksauth/doc.go, jwksauth/README.md
    • Human review: spot-check of the diff before commit (not line-by-line)

Change classification

  • Leaf node (local impact)
  • Core code (broad impact — needs line-by-line review)

Claims is a public SDK type consumed by every caller of jwksauth.Verifier / Middleware. The change is purely additive and backwards-compatible (new field defaults to ""), but reviewers should still confirm: (a) wire-key string "_uid" matches what upstream AuthGate emits, (b) Extras-exclusion logic correctly suppresses the <prefix>_uid key, (c) staticReservedClaimKeys is intentionally untouched (the user_id UUID concept is separate from UID username and must not be disturbed).

Plan reference

plan.md in the repo root (not committed; planning artifact). Goal: surface the username claim from user-bearing AuthGate flows on Claims.UID, keep staticReservedClaimKeys.user_id (the UUID concept) untouched, and use the same hard-cutover prefix semantics as the other three server-attested claims.

Verification

  • Unit tests (decoder behavior via Verifier.Verify and through Middleware)
  • At least 3 e2e tests (1 happy path + 2 errors / edges)
  • Stress / soak test: N/A (pure additive decoder change, no concurrency / async paths added)

Test inventory:

  1. TestPrefixedClaims_DefaultPrefix_HappyPath — extended: token carries extra_uid: "alice"; asserts Claims.UID == "alice" (user-bearing happy path).
  2. TestVerifier_HappyPath — extended: same as above, asserted on the verifier directly.
  3. TestVerifier_ClientCredentialsShape_UIDEmpty — new: token without extra_uid succeeds, Claims.UID == "", middleware returns 200 under AccessRule{} (Client Credentials shape).
  4. TestPrefixedClaims_CustomPrefix/acme_prefix_hits — extended: acme_uid correctly populates Claims.UID.
  5. TestPrefixedClaims_CustomPrefix/default_prefix_no_fallback — extended: under acme prefix, extra_uid does NOT fall back, lands in Extras instead, Claims.UID == "" (hard cutover).
  6. TestPrefixedClaims_BareUIDIgnored — new: bare uid key (no <prefix>_) is not promoted to Claims.UID; surfaces via Extras["uid"] instead.

make lint clean (0 issues). make test green across all packages.

Manual verification: none required (pure library change, no runtime behavior to inspect outside the test suite).

Verifiability check

  • Inputs and outputs are documented (godoc on Claims, claimKeys, newClaimKeys; doc.go and README.md describe the four server-attested claims and the Client-Credentials = empty UID invariant)
  • Reviewer can judge correctness from the test names + assertions without re-deriving the prefix scheme
  • Failure mode is observable: a token whose username doesn't appear at info.Claims.UID after this change indicates wire-key drift

Security check

  • No secrets in code (test claim values like alice, oa, p1 are not credentials)
  • All external inputs validated — verification path (signature, iss, aud, exp, nbf) is unchanged
  • No new permission surface — AccessRule deliberately does not gain a UIDs field; UID is identity, not authorization. Callers gate on UID in their own handler if needed.
  • Errors don't leak internals — UID is treated identically to existing server-attested fields, no new error paths

Risk & rollback

  • Risk 1 — wire-key mismatch: if upstream AuthGate emits the username under a key other than <prefix>_uid (e.g. <prefix>_username), Claims.UID will silently always be empty. Cost: dead field, no incorrect behavior.
  • Risk 2 — caller-supplied key collision: an existing caller using extra_uid as a custom Extras key will see that key disappear from Extras and surface on Claims.UID instead. This is the documented <prefix>_* namespace contract (same for Domain/Project/ServiceAccount) and was already a "do not use" zone for callers.
  • Rollback: revert this commit. Pure additive change, no schema migration, no state change.

Reviewer guide

  • Read carefully:
    • jwksauth/claims.go — confirm wire key "_uid" (line 125), confirm keys.uid is included in the Extras-exclusion check (line 150), confirm staticReservedClaimKeys is untouched (the user_id entry there is the separate UUID concept and must not be disturbed).
    • jwksauth/claims_prefix_test.go:184TestPrefixedClaims_BareUIDIgnored and the new assertions in the default_prefix_no_fallback subtest pin the hard-cutover semantics.
  • Spot-check OK:
    • jwksauth/middleware_test.go — additions mirror existing patterns (extra_domain / extra_project style).
    • jwksauth/doc.go, jwksauth/README.md — text-only updates aligned with the code change.

🤖 Generated with Claude Code

- Add UID field to Claims, populated from the <prefix>_uid JWT key
- Thread UID through claimKeys, newClaimKeys, and newTokenInfo alongside Domain, Project, and ServiceAccount, preserving lazy Extras allocation
- Document that UID is the username for user-bearing flows and is empty for Client Credentials tokens
- Cover happy path, custom-prefix hard cutover, bare-key ignore, and Client Credentials shape with new tests
- Refresh doc.go and README to describe four server-attested claims

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 9, 2026 08:59
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 support in jwksauth for a fourth server-attested private claim, UID, surfaced as Claims.UID and sourced from the <prefix>_uid JWT payload key (default extra_uid). This extends the existing server-attested claim plumbing (prefix resolution + Extras exclusion) alongside Domain/Project/ServiceAccount and updates tests/docs accordingly.

Changes:

  • Extend jwksauth.Claims and internal claimKeys/decoding to read <prefix>_uid into Claims.UID and exclude it from Extras.
  • Add/extend unit tests covering default/custom prefix behavior, no-fallback semantics, and “client credentials = empty UID”.
  • Update package docs and README to document the new claim and its semantics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
jwksauth/claims.go Adds Claims.UID, threads <prefix>_uid through claimKeys and decoding, and excludes it from Extras.
jwksauth/claims_prefix_test.go Extends prefix/Extras tests to cover UID, including hard-cutover and bare uid behavior.
jwksauth/middleware_test.go Adds verifier/middleware test coverage for UID and client-credentials shape.
jwksauth/doc.go Updates package-level documentation to include UID as server-attested under the prefix.
jwksauth/README.md Updates README to describe extra_uid / Claims.UID and related semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread jwksauth/claims.go
Comment thread jwksauth/README.md Outdated
- Rephrase the wrong-prefix sentence to make explicit that fail-closed only applies to the three AccessRule-covered dimensions, and that UID is identity not authorization

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@appleboy appleboy requested a review from Copilot May 9, 2026 09:09
@appleboy appleboy merged commit 9ab6750 into main May 9, 2026
19 of 20 checks passed
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 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread jwksauth/claims.go
Comment on lines 30 to 37
type Claims struct {
ClientID string
Scope string
Domain string
ServiceAccount string
Project string
UID string

Comment thread jwksauth/doc.go
Comment on lines +37 to +45
// AuthGate emits four private claims — Domain, Project, ServiceAccount,
// and UID — under a configurable prefix (default "extra"), so the JWT
// payload keys are "extra_domain", "extra_project", "extra_service_account",
// and "extra_uid". The SDK reads them out of the box; if your AuthGate
// deployment has overridden JWT_PRIVATE_CLAIM_PREFIX, pass the same value
// via [WithPrivateClaimPrefix]. UID carries the username for tokens
// issued by user-bearing flows (Authorization Code + PKCE, Device
// Authorization Grant); the Client Credentials flow has no user, so UID
// is the empty string for those tokens.
Comment on lines 76 to +80
t.Run("acme_prefix_hits", func(t *testing.T) {
tok := fi.Sign(t, "api://x", time.Minute, map[string]any{"acme_domain": "oa"})
tok := fi.Sign(t, "api://x", time.Minute, map[string]any{
"acme_domain": "oa",
"acme_uid": "alice",
})
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