✨ Add authlib/ — shared token exchange building blocks (Phase 1)#310
✨ Add authlib/ — shared token exchange building blocks (Phase 1)#310huang195 wants to merge 5 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
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 ✓
| jwt.WithValidate(true), | ||
| jwt.WithIssuer(v.issuer), | ||
| } | ||
| if audience != "" { |
There was a problem hiding this comment.
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))There was a problem hiding this comment.
Fixed in b2ca966. Empty audience now returns an error: "audience is required (prevents confused deputy attacks)". Added TestJWKSVerifier_EmptyAudience_Rejected test to verify.
| form.Set("actor_token_type", "urn:ietf:params:oauth:token-type:access_token") | ||
| } | ||
|
|
||
| if err := c.auth.Apply(ctx, form, nil); err != nil { |
There was a problem hiding this comment.
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:
- Passing
req.Headerafter creating the request (requires restructuringdoTokenRequest), or - Passing
make(http.Header)and merging into the request afterward, or - Removing the
headersparameter from theClientAuthinterface if it's not needed for Phase 1
Same pattern at line 96 in ClientCredentials().
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…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>
…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
left a comment
There was a problem hiding this comment.
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 ✓
| // 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 TODO080f19f: Replace t.Fatal with http.Error in httptest handlers (exchange/client_test.go)
All 40 tests passing across 6 packages.
|
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. |
|
@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. |
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:
validation/lestrrat-go/jwx)exchange/cache/bypass/spiffe/routing/Key improvements over current go-processor:
Verify()(prevents confused deputy attacks)ExchangeErrorwith OAuth error/error_description fieldsRelated issue(s)
Ref #279
Test plan
go test ./...— 34 tests passinggo vet ./...— no issuesAssisted-By: Claude (Anthropic AI) noreply@anthropic.com