Skip to content

✨ Add auth/ and config/ — composition layer and mode presets (Phase 2)#311

Merged
pdettori merged 10 commits into
rossoctl:mainfrom
huang195:feat/authlib-phase2
Apr 15, 2026
Merged

✨ Add auth/ and config/ — composition layer and mode presets (Phase 2)#311
pdettori merged 10 commits into
rossoctl:mainfrom
huang195:feat/authlib-phase2

Conversation

@huang195

Copy link
Copy Markdown
Member

Summary

Phase 2 of the modular token exchange library (#279). Adds the application layer that composes Phase 1 authlib building blocks into two functions all listeners will call.

Depends on: #310 (Phase 1)

auth/ — Composition layer

Function Flow
HandleInbound bypass check → extract bearer → validate JWT → allow/deny
HandleOutbound resolve route → cache check → token exchange → cache result → allow/deny/replace

Key features:

  • Per-request audience override for waypoint mode (derived from destination host)
  • Actor token source for RFC 8693 Section 4.1 act claim chaining
  • Per-route token endpoint override (different IdP per target service)
  • Fail-closed: nil verifier denies requests (not silently allows)
  • Case-insensitive bearer extraction (RFC 7235 compliance)
  • Three no-token policies: client-credentials (envoy-sidecar), allow (waypoint), deny (proxy-sidecar)

config/ — Mode presets and validation

  • YAML config with ${ENV_VAR} expansion (preserves undefined refs as-is)
  • Three mode presets with locked vs user-overridable settings
  • Startup validation rejects invalid combos (e.g., waypoint + ext-proc listener)
  • Resolve() wires all authlib packages from a single config file
  • Backwards-compatible route config (passthrough: true to action: passthrough)

Phase 1 changes

  • exchange.ExchangeRequest.TokenEndpoint — per-request token URL override

Related issue(s)

Ref #279

Test plan

  • go test ./... — 56 tests passing across 8 packages
  • 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>
Add the application layer that wires Phase 1 authlib building blocks
into two functions all listeners will call:

auth/:
- HandleInbound: bypass → extract bearer → validate JWT → allow/deny
- HandleOutbound: resolve route → cache check → exchange → cache result
- Per-request audience override for waypoint mode
- Actor token source for RFC 8693 act claim chaining
- Per-route token endpoint override
- Fail-closed: nil verifier denies (not allows)
- Case-insensitive bearer token extraction (RFC 7235)

config/:
- YAML config with ${ENV_VAR} expansion (preserves undefined refs)
- Three mode presets: envoy-sidecar, waypoint, proxy-sidecar
- Startup validation rejects invalid mode+listener combos
- Resolve() wires all authlib packages from config
- RouteConfig backwards compat (passthrough: true → action: passthrough)

Also updates exchange.Client to support per-request TokenEndpoint override
and ExchangeRequest.TokenEndpoint field.

56 tests across 8 packages, all passing.

Ref: rossoctl#279

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Close 4 feature parity gaps between old go-processor and new authbridge:

- Derive TOKEN_URL and ISSUER from keycloak_url + keycloak_realm
  (operator sets these, not explicit URLs)
- Derive JWKS URL from TOKEN_URL using Keycloak /token -> /certs convention
- Support client_id_file and client_secret_file with startup polling
  (handles race with client-registration and spiffe-helper)
- Add CachedActorTokenSource for actor token caching with 30s
  pre-expiry refresh (matches old actorTokenCache behavior)

7 new tests for derivation logic and file-based credentials.
63 total tests passing.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Two feature parity findings from comprehensive review:

1. ClientCredentials grant now passes audience to Keycloak (matching
   old go-processor behavior). Without this, Keycloak may issue tokens
   with wrong audience that target services reject.

2. Waypoint mode now auto-derives audience from destination hostname
   via AudienceDeriver callback (serviceNameFromHost). This eliminates
   the need for explicit routes in waypoint mode, matching the old
   waypoint behavior where audience = first DNS label of host.

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 2 adding the auth composition layer (auth/, config/) on top of Phase 1 building blocks. The auth flow logic (HandleInbound, HandleOutbound) is clean and follows fail-closed principles for inbound validation. One correctness bug in CachedActorTokenSource where the cache degrades after the first TTL expiry. Test helpers use t.Fatal from handler goroutines which is technically undefined behavior in Go.

Areas reviewed: Go (auth, config, exchange, routing, bypass, cache, spiffe, validation), Tests
Commits: 6 commits, all signed-off ✅
CI status: All 15 checks passing ✅

Highlights

  • Clean six-package decomposition with single-responsibility packages and well-defined interfaces
  • Strong security defaults: fail-closed inbound (nil verifier denies), path normalization in bypass, SHA-256 cache keys, 1MB response body limit, 30s pre-expiry cache buffer
  • 63 tests with good edge case coverage (case-insensitive bearer, path traversal, context cancellation, cache eviction)
  • Good backwards compatibility (passthrough: trueaction: passthrough)

Comment thread authbridge/authlib/auth/actor_cache.go Outdated
// The actual TTL is set via SetTTL after the client-credentials grant.
if c.expiresAt.IsZero() {
c.expiresAt = time.Now().Add(5 * time.Minute)
}

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.

Bug: if c.expiresAt.IsZero() only matches on the very first call. After the initial 5-minute default TTL expires, the refresh path stores a new token (line 50) but does NOT update expiresAt — it's no longer zero, so this guard is skipped.

Result: every subsequent FetchToken call sees the stale expiresAt, treats the token as expired, and calls source(ctx) on every request. The cache degrades to no-caching after the first TTL.

Fix: always reset expiresAt after refresh:

c.token = token
c.expiresAt = time.Now().Add(5 * time.Minute) // default, SetTTL overrides

This type isn't wired up in Phase 2 but will be in Phase 3 — better to fix now before it ships.

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.

Good catch. Fixed in 8cf07a1expiresAt is now always reset after refresh:

c.token = token
c.expiresAt = time.Now().Add(5 * time.Minute)

The IsZero() guard was wrong — after the first default TTL expired, subsequent refreshes never updated expiresAt, degrading the cache to no-caching.

Identity: IdentityConfig{Audience: "my-agent"},
})
result := a.HandleInbound(context.Background(), "Bearer valid-token", "/api/test", "")
if result.Action != ActionAllow {

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: t.Fatal() called from an httptest.Server handler goroutine has undefined behavior in Go — it calls runtime.Goexit() on the wrong goroutine, which can cause test panics or hangs.

This pattern appears here and in several handlers in exchange/client_test.go.

Fix: use t.Error(err); http.Error(w, err.Error(), 500); return instead of t.Fatal(err) in all httptest handlers.

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 8cf07a1. Replaced all t.Fatal(err) in httptest handlers with http.Error(w, err.Error(), http.StatusBadRequest); return. Applied to both auth/auth_test.go and exchange/client_test.go.

Comment thread authbridge/authlib/config/resolve.go Outdated
jwksURL := strings.Replace(cfg.Outbound.TokenURL, "/token", "/certs", 1)
if jwksURL != cfg.Outbound.TokenURL {
cfg.Inbound.JWKSURL = jwksURL
slog.Info("derived jwks_url from token_url", "jwks_url", jwksURL)

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: strings.Replace(..., "/token", "/certs", 1) replaces the first occurrence. A URL like http://token-service.example.com/.../token would incorrectly modify the hostname portion.

In practice this is safe because deriveKeycloakURLs builds clean URLs, but a suffix-based approach would be more robust:

if strings.HasSuffix(cfg.Outbound.TokenURL, "/token") {
    cfg.Inbound.JWKSURL = strings.TrimSuffix(cfg.Outbound.TokenURL, "/token") + "/certs"
}

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 8cf07a1. Switched to suffix-based approach:

if strings.HasSuffix(cfg.Outbound.TokenURL, "/token") {
    cfg.Inbound.JWKSURL = strings.TrimSuffix(cfg.Outbound.TokenURL, "/token") + "/certs"
}

This prevents accidentally modifying hostnames like http://token-service.example.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>
Address review from @pdettori:
- Extract ServiceNameFromHost to routing/hostutil.go (shared by
  config/resolve.go and extauthz listener, avoids duplication)
- Add TODO comment on SetTTL (unused, clarifies intent)

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
JWT validation errors can reveal issuer URL, audience, or algorithm
details. Return 'token validation failed' to the client and log the
full error server-side at INFO level.

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
Address review from @pdettori on PR rossoctl#313:
- Add ReadHeaderTimeout: 10s to HTTP servers (slowloris defense)
- Add pipefail to entrypoint.sh set flags

Findings 1 (JWT error leakage) fixed in PR rossoctl#311. Findings 2 (GracefulStop)
and 4 (Envoy SHA pin) already fixed in PR rossoctl#312 review round.

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
…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 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
Address review from @pdettori on PR rossoctl#313:
- Add ReadHeaderTimeout: 10s to HTTP servers (slowloris defense)
- Add pipefail to entrypoint.sh set flags

Findings 1 (JWT error leakage) fixed in PR rossoctl#311. Findings 2 (GracefulStop)
and 4 (Envoy SHA pin) already fixed in PR rossoctl#312 review round.

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>
huang195 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
Address review from @pdettori on PR rossoctl#313:
- Add ReadHeaderTimeout: 10s to HTTP servers (slowloris defense)
- Add pipefail to entrypoint.sh set flags

Findings 1 (JWT error leakage) fixed in PR rossoctl#311. Findings 2 (GracefulStop)
and 4 (Envoy SHA pin) already fixed in PR rossoctl#312 review round.

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
…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 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
Address review from @pdettori on PR rossoctl#313:
- Add ReadHeaderTimeout: 10s to HTTP servers (slowloris defense)
- Add pipefail to entrypoint.sh set flags

Findings 1 (JWT error leakage) fixed in PR rossoctl#311. Findings 2 (GracefulStop)
and 4 (Envoy SHA pin) already fixed in PR rossoctl#312 review round.

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.

Approved

All 3 review findings addressed in 8cf07a1:

  1. actor_cache.goexpiresAt now always reset after refresh (was stale after first TTL) ✅
  2. auth_test.go / client_test.got.Fatal in httptest handlers replaced with http.Error + return
  3. resolve.go — JWKS URL derivation uses HasSuffix/TrimSuffix instead of strings.Replace

PR #310 fixes also landed via b2ca966: audience enforcement, header param removal, 6 JWKSVerifier tests, TOCTOU fix, cache eviction TODO, SubjectToken validation.

Additional proactive improvements: ServiceNameFromHost extracted to shared package, generic JWT errors returned to clients (detail logged server-side).

10 commits, all signed-off. 15 CI checks passing. Clean Phase 2.

@pdettori
pdettori merged commit e44098c into rossoctl:main Apr 15, 2026
15 checks passed
huang195 added a commit to huang195/kagenti-extensions that referenced this pull request Apr 15, 2026
…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 deleted the feat/authlib-phase2 branch May 20, 2026 18:05
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