Skip to content

✨ Add authlib/ — shared token exchange building blocks (Phase 1)#310

Closed
huang195 wants to merge 5 commits into
rossoctl:mainfrom
huang195:feat/authlib-phase1
Closed

✨ Add authlib/ — shared token exchange building blocks (Phase 1)#310
huang195 wants to merge 5 commits into
rossoctl:mainfrom
huang195:feat/authlib-phase1

Conversation

@huang195

Copy link
Copy Markdown
Member

Summary

Phase 1 of the modular token exchange library (#279). Extracts reusable auth building blocks from go-processor into a standalone Go module at authbridge/authlib/.

Six packages, 34 unit tests, zero Envoy/gRPC dependencies:

Package Purpose Tests
validation/ JWKS-backed JWT verifier (lestrrat-go/jwx) 3
exchange/ RFC 8693 token exchange + client credentials 5
cache/ SHA-256 keyed token cache with TTL eviction 7
bypass/ Path pattern matcher for public endpoints 5
spiffe/ File-based JWT-SVID source 4
routing/ Host-to-audience router with glob matching 10

Key improvements over current go-processor:

  • Audience is a required parameter to Verify() (prevents confused deputy attacks)
  • Exchanged-token cache (go-processor has none — every exchange hits the IdP)
  • Configurable HTTP client with 30s timeout (go-processor uses default — no timeout)
  • Typed ExchangeError with OAuth error/error_description fields
  • Instance-based, not global mutable state
  • Pure Go errors, no gRPC coupling

Related issue(s)

Ref #279

Test plan

  • go test ./... — 34 tests passing
  • go vet ./... — no issues
  • CI passes

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Extract reusable auth building blocks from go-processor into a standalone
Go module at authbridge/authlib/. Six packages with 34 unit tests:

- validation/ — JWKS-backed JWT verifier (lestrrat-go/jwx)
- exchange/   — RFC 8693 token exchange + client credentials
- cache/      — SHA-256 keyed token cache with TTL eviction
- bypass/     — Path pattern matcher for public endpoints
- spiffe/     — File-based JWT-SVID source
- routing/    — Host-to-audience router with glob matching

No Envoy/gRPC dependencies — pure Go library. Phase 2 will add the
auth composition layer and config; Phase 3 wires up listeners.

Ref: rossoctl#279

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
- exchange: Use http.NewRequestWithContext instead of PostForm so
  callers can cancel/timeout requests via context
- exchange: Limit response body to 1MB via io.LimitReader
- exchange: Add tests for connection failure, malformed JSON, and
  context cancellation
- cache: Remove TOCTOU race in Get() — skip expired entry cleanup,
  rely on Set() eviction instead
- routing: Add Matched field to ResolvedRoute so callers can
  distinguish explicit route match from default action fallback
- bypass: Document path.Match limitation (* does not cross /)

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Transitive dependency golang.org/x/crypto v0.32.0 has a known DoS
vulnerability flagged by the dependency review CI check. Bump to
v0.48.0 to match the existing authproxy module.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review Summary

Well-structured Phase 1 library with clean interfaces, minimal dependencies (jwx, gobwas/glob, yaml.v3), and strong test coverage (34 tests across 6 packages). The security-conscious design — SHA-256 cache keys, path normalization, response size limits, context propagation — is solid.

Highlights:

  • Cache key collision resistance test and concurrent access test — good security-conscious testing
  • Bypass matcher path normalization tests (double slash, dot segments, parent traversal)
  • Exchange client edge case coverage: context cancellation, malformed JSON, connection failure

One must-fix on the audience validation contract, plus a couple of suggestions for Phase 2 readiness.

Areas reviewed: Go code (all 19 files), go.mod dependencies, security, tests
Commits: 3, all signed-off ✓
CI: All checks passing ✓

Comment thread authbridge/authlib/validation/jwks.go Outdated
jwt.WithValidate(true),
jwt.WithIssuer(v.issuer),
}
if audience != "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

must-fix: The PR description states "Audience is a required parameter to Verify() (prevents confused deputy attacks)" and the Verifier interface doc says "audience is a required parameter". But passing an empty string silently skips the audience check:

if audience != "" {
    parseOpts = append(parseOpts, jwt.WithAudience(audience))
}

This creates a silent failure mode: a caller who accidentally passes "" gets full JWT validation minus audience — exactly the confused deputy scenario this library is designed to prevent.

Suggestion: Return an error when audience is empty to match the documented invariant:

if audience == "" {
    return nil, fmt.Errorf("audience is required (prevents confused deputy attacks)")
}
parseOpts = append(parseOpts, jwt.WithAudience(audience))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b2ca966. Empty audience now returns an error: "audience is required (prevents confused deputy attacks)". Added TestJWKSVerifier_EmptyAudience_Rejected test to verify.

Comment thread authbridge/authlib/exchange/client.go Outdated
form.Set("actor_token_type", "urn:ietf:params:oauth:token-type:access_token")
}

if err := c.auth.Apply(ctx, form, nil); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion: c.auth.Apply(ctx, form, nil) passes nil for the http.Header parameter. The ClientAuth interface accepts headers (for use cases like Basic auth or bearer token injection), but a nil value means any future implementation that calls headers.Set(...) would panic with a nil pointer dereference.

Current implementations ignore the header parameter so this works today. Consider either:

  1. Passing req.Header after creating the request (requires restructuring doTokenRequest), or
  2. Passing make(http.Header) and merging into the request afterward, or
  3. Removing the headers parameter from the ClientAuth interface if it's not needed for Phase 1

Same pattern at line 96 in ClientCredentials().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b2ca966. Removed http.Header param from ClientAuth.Apply() — simplified to Apply(ctx, form). Eliminates the nil pointer risk. Header support can be added via a separate ApplyHeaders method if needed later.

}
}

// Note: Integration tests for JWKSVerifier.Verify() require a running JWKS endpoint

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

suggestion: The core security path — JWKSVerifier.Verify() — has zero test coverage. The Claims helper tests are good, but the actual JWT validation (signature verification, expiry, issuer check, audience enforcement) is untested.

I understand integration tests are deferred to Phase 2, but consider adding a unit test with an in-memory JWKS server (using httptest.NewServer + jwk.NewSet()) to cover at least:

  • Valid token → success
  • Expired token → error
  • Wrong issuer → error
  • Wrong audience → error
  • Invalid signature → error

This would catch regressions in the parse options wiring (like the audience bypass above) without needing Keycloak.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in b2ca966. Added 6 JWKSVerifier unit tests with in-memory httptest JWKS server: valid token, expired token, wrong issuer, wrong audience, empty audience (rejected), invalid signature. These cover the core security path without needing Keycloak.

if len(c.entries) >= c.maxSize {
c.evictExpired()
if len(c.entries) >= c.maxSize {
c.entries = make(map[string]entry)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: The nuclear eviction (clear all entries) when the cache is full and no expired entries exist could cause temporary cache-miss storms under sustained high-cardinality traffic. Not a problem for Phase 1 volumes, but worth a // TODO: consider LRU or random-sample eviction for Phase 2 marker so it doesn't get forgotten when this is wired into the hot path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added TODO comment in b2ca966. The nuclear eviction is acceptable for Phase 1 volumes. Will consider LRU or random-sample eviction when this is wired into the hot path.

huang195 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
…JWKS tests

Address review from @pdettori on PR rossoctl#310:

1. MUST-FIX: Empty audience now returns error in JWKSVerifier.Verify()
   instead of silently skipping the check. Enforces the confused deputy
   protection invariant stated in the interface doc.

2. Remove http.Header param from ClientAuth.Apply() interface — callers
   always passed nil, risking NPE for future header-based implementations.
   Simplified to form-only. Add header support later if needed.

3. Add 6 JWKSVerifier unit tests with in-memory httptest JWKS server:
   valid token, expired, wrong issuer, wrong audience, empty audience,
   invalid signature. Covers the core security path.

4. Validate SubjectToken != empty in Exchange() for clear early error.

5. Fix TOCTOU in LoadRoutes — call ReadFile directly, check IsNotExist.

6. Add TODO for cache eviction strategy (thundering herd on full clear).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
huang195 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
…JWKS tests

Address review from @pdettori on PR rossoctl#310:

1. MUST-FIX: Empty audience now returns error in JWKSVerifier.Verify()
   instead of silently skipping the check. Enforces the confused deputy
   protection invariant stated in the interface doc.

2. Remove http.Header param from ClientAuth.Apply() interface — callers
   always passed nil, risking NPE for future header-based implementations.
   Simplified to form-only. Add header support later if needed.

3. Add 6 JWKSVerifier unit tests with in-memory httptest JWKS server:
   valid token, expired, wrong issuer, wrong audience, empty audience,
   invalid signature. Covers the core security path.

4. Validate SubjectToken != empty in Exchange() for clear early error.

5. Fix TOCTOU in LoadRoutes — call ReadFile directly, check IsNotExist.

6. Add TODO for cache eviction strategy (thundering herd on full clear).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Follow-up Review

Fixes not pushed: All 4 comments from the prior review were addressed by the author in replies citing commit b2ca966, but that commit is not in the PR branch. The branch still has only the original 3 commits (4603cdb, ead07d9, ed00dab). The must-fix (empty audience bypass in validation/jwks.go:70) and the 3 suggestions are still present in the code.

Please push the fix commit so the changes can be verified and the PR approved.

One additional minor finding below.

Areas reviewed: Go code (all 19 files), go.mod dependencies, security, tests
Commits: 3, all signed-off ✓
CI: All checks passing ✓

Comment thread authbridge/authlib/routing/config.go Outdated
// LoadRoutes loads routes from a YAML file.
// Returns an empty slice (not error) if the file doesn't exist.
func LoadRoutes(path string) ([]Route, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: TOCTOU race — os.Stat then os.ReadFile could give inconsistent results if the file is created/deleted between the two calls. The idiomatic Go pattern avoids this:

data, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
    return nil, nil
}
if err != nil {
    return nil, fmt.Errorf("reading routes config: %w", err)
}

Simpler and race-free. Not blocking — just a cleanup for whenever.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in a5623ea (cherry-picked onto Phase 1 branch). Uses os.ReadFile directly with errors.Is(err, os.ErrNotExist) check.

…JWKS tests

Address review from @pdettori on PR rossoctl#310:

1. MUST-FIX: Empty audience now returns error in JWKSVerifier.Verify()
   instead of silently skipping the check. Enforces the confused deputy
   protection invariant stated in the interface doc.

2. Remove http.Header param from ClientAuth.Apply() interface — callers
   always passed nil, risking NPE for future header-based implementations.
   Simplified to form-only. Add header support later if needed.

3. Add 6 JWKSVerifier unit tests with in-memory httptest JWKS server:
   valid token, expired, wrong issuer, wrong audience, empty audience,
   invalid signature. Covers the core security path.

4. Validate SubjectToken != empty in Exchange() for clear early error.

5. Fix TOCTOU in LoadRoutes — call ReadFile directly, check IsNotExist.

6. Add TODO for cache eviction strategy (thundering herd on full clear).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
…ation

Address review from @pdettori on PR rossoctl#311:

1. actor_cache.go: Always reset expiresAt after token refresh. The
   IsZero() guard only triggered on the first call; subsequent refreshes
   left expiresAt stale, degrading the cache to no-caching.

2. auth_test.go, client_test.go: Replace t.Fatal(err) with
   http.Error() + return in httptest handlers. t.Fatal calls
   runtime.Goexit on the wrong goroutine, causing undefined behavior.

3. resolve.go: Use suffix-based HasSuffix + TrimSuffix for JWKS URL
   derivation instead of strings.Replace. Prevents accidentally
   modifying hostnames containing "token".

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>

@huang195 huang195 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixes now pushed to this branch (cherry-picked from Phase 2):

  • a5623ea: Enforce non-empty audience in Verify(), remove http.Header from ClientAuth.Apply(), add 6 JWKSVerifier unit tests, validate SubjectToken, fix TOCTOU in LoadRoutes, add cache eviction TODO
  • 080f19f: Replace t.Fatal with http.Error in httptest handlers (exchange/client_test.go)

All 40 tests passing across 6 packages.

@huang195

Copy link
Copy Markdown
Member Author

All Phase 1 code is now on main via the Phase 2 (#311) and Phase 3 (#312) merges, which included the Phase 1 commits in their history plus all review fixes. This PR can be closed as superseded.

The review findings (enforce audience, remove nil Header, JWKS tests, TOCTOU fix, SubjectToken validation, t.Fatal in handlers, cache eviction TODO) are all on main.

@huang195

Copy link
Copy Markdown
Member Author

@pdettori thanks for the review. since phase 2 and phase 3 PRs are already merged, and phase 1 PR is already included in those, I'm just going to close this PR as we have everything we need.

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