Plan: Modular Token Exchange Library
Context
We have three approaches to token exchange (AuthBridge, Klaviger, Waypoint) that all implement the same core logic independently — JWT validation, RFC 8693 exchange, token caching, bypass paths. The code analysis (docs/code-analysis.md) shows 80%+ overlap across all three codebases. This refactor extracts the shared logic into a Go library that all three approaches import, with thin adapter layers for each interface and interception mechanism.
Architecture
┌─────────────────────────────────────────────────┐
│ pkg/tokenexchange │
│ │
│ core/ - evaluateAuth(), authDecision │
│ jwt/ - JWKS cache, JWT validation │
│ exchange/ - RFC 8693 client, token cache │
│ bypass/ - path matching │
│ identity/ - agent identity resolution │
│ │
├─────────────────────────────────────────────────┤
│ pkg/adapter │
│ │
│ extauthz/ - Envoy ext_authz gRPC v3 │
│ extproc/ - Envoy ext_proc gRPC v3 │
│ httpproxy/ - HTTP forward proxy │
│ reverseproxy/ - HTTP reverse proxy │
│ │
├─────────────────────────────────────────────────┤
│ cmd/ │
│ │
│ waypoint/ - ext_authz │
│ authbridge/ - ext_proc │
│ klaviger/ - httpproxy + reverseproxy │
│ │
└─────────────────────────────────────────────────┘
Package Design
pkg/tokenexchange/core
The single decision function, shared by all adapters:
type AuthDecision struct {
Action Action // Allow, AllowWithToken, Deny
Token string // exchanged token (if AllowWithToken)
DenyReason string
DenyCode int
}
type Config struct {
// JWT validation
JWKSProvider jwt.Provider
IssuerURL string
// Token exchange
ExchangeClient exchange.Client
// Bypass
BypassPaths []string
// Agent identity (optional)
IdentityResolver identity.Resolver
}
type Engine struct { ... }
func NewEngine(cfg Config) *Engine
func (e *Engine) Evaluate(ctx context.Context, req *Request) *AuthDecision
Request is adapter-neutral:
type Request struct {
AuthHeader string // Authorization header value
Host string // destination hostname
Path string // request path
SourceIP string // caller IP (for agent identity in proxy mode)
SourcePrincipal string // SPIFFE ID (for agent identity in waypoint mode)
}
pkg/tokenexchange/jwt
JWKS fetching and JWT validation. Abstracts the differences:
type Provider interface {
Validate(ctx context.Context, tokenStr string) (*Claims, error)
}
type Claims struct {
Subject string
Issuer string
Audience []string
ClientID string // azp
Scopes []string
ExpiresAt time.Time
Extra map[string]any
}
// Implementations:
func NewKeycloakProvider(jwksURL, issuerURL string) Provider // JWKS + issuer check
func NewK8sProvider(apiServer string) Provider // TokenReview API
func NewIntrospectionProvider(endpoint string) Provider // RFC 7662
Uses lestrrat-go/jwx (already used by AuthBridge and Klaviger) instead of manual RSA parsing. Supports RSA + EC keys.
pkg/tokenexchange/exchange
RFC 8693 token exchange client:
type Client interface {
Exchange(ctx context.Context, req *ExchangeRequest) (*ExchangeResponse, error)
}
type ExchangeRequest struct {
SubjectToken string
Audience string
Scope string // optional
ActorToken string // optional (RFC 8693 delegation)
}
type ExchangeResponse struct {
AccessToken string
ExpiresIn int
TokenType string
}
type ClientAuth interface {
Apply(form url.Values, headers http.Header)
}
// Client auth implementations:
func NewClientSecretAuth(clientID, clientSecret string) ClientAuth
func NewJWTBearerAuth(tokenSource func() (string, error)) ClientAuth // K8s SA token
func NewSPIFFEAuth(socketPath string, audience []string) ClientAuth // SPIFFE JWT-SVID
Includes token cache (SHA-256 keys, TTL, periodic eviction).
pkg/tokenexchange/bypass
Path matching (identical in Waypoint and AuthBridge, hardcoded in Klaviger):
func NewMatcher(patterns []string) *Matcher
func (m *Matcher) Match(path string) bool // strips query, path.Clean, path.Match
pkg/tokenexchange/identity
Agent identity resolution for the shared service model:
type Resolver interface {
Resolve(ctx context.Context, req *Request) (string, error) // returns agent identity string
}
func NewSPIFFEResolver() Resolver // from ext_authz source principal
func NewSourceIPResolver(k8sClient) Resolver // source IP → pod → service account
func NewStaticResolver(identity string) Resolver // for sidecar (always the pod's identity)
pkg/adapter/extauthz
Envoy ext_authz gRPC v3 adapter (for waypoint mode):
func NewServer(engine *core.Engine) auth.AuthorizationServer
Translates CheckRequest → core.Request, calls engine.Evaluate(), translates AuthDecision → CheckResponse.
pkg/adapter/extproc
Envoy ext_proc gRPC v3 adapter (for AuthBridge sidecar mode):
func NewServer(engine *core.Engine, cfg ExtProcConfig) ext_proc.ExternalProcessorServer
Handles bidirectional streaming. Detects direction from x-authbridge-direction header. Inbound → validate. Outbound → exchange.
pkg/adapter/httpproxy
HTTP forward proxy adapter (for shared proxy and Klaviger outbound):
func NewHandler(engine *core.Engine) http.Handler
Receives proxied HTTP requests, calls engine.Evaluate(), replaces Authorization header, forwards to destination.
pkg/adapter/reverseproxy
HTTP reverse proxy adapter (for Klaviger inbound and potential shared proxy inbound):
func NewHandler(engine *core.Engine, backend string) http.Handler
Receives inbound requests, calls engine.Evaluate() for validation, forwards to backend on localhost.
What each binary assembles
cmd/waypoint (current token-exchange-service)
engine := core.NewEngine(core.Config{
JWKSProvider: jwt.NewKeycloakProvider(jwksURL, issuerURL),
ExchangeClient: exchange.NewClient(tokenURL, exchange.NewClientSecretAuth(clientID, clientSecret)),
BypassPaths: defaultBypassPaths,
IdentityResolver: identity.NewSPIFFEResolver(),
})
extauthz.Serve(engine, ":9090")
cmd/authbridge (go-processor replacement)
engine := core.NewEngine(core.Config{
JWKSProvider: jwt.NewKeycloakProvider(jwksURL, issuerURL),
ExchangeClient: exchange.NewClient(tokenURL, exchange.NewSPIFFEAuth(socketPath, audience)),
BypassPaths: bypassPaths,
IdentityResolver: identity.NewStaticResolver(agentID),
})
extproc.Serve(engine, ":8081")
cmd/klaviger (klaviger replacement)
engine := core.NewEngine(core.Config{
JWKSProvider: jwt.NewKeycloakProvider(jwksURL, issuerURL),
ExchangeClient: exchange.NewClient(tokenURL, exchange.NewJWTBearerAuth(k8sTokenSource)),
BypassPaths: bypassPaths,
IdentityResolver: identity.NewStaticResolver(agentID),
})
// Two listeners, same engine
go httpproxy.Serve(engine, ":8081") // outbound
go reverseproxy.Serve(engine, backend, ":8080") // inbound
Repo structure
This repo becomes the single home for all token exchange code. Each approach is a cmd/ binary:
authbridge-waypoint/
├── pkg/
│ ├── tokenexchange/ # shared library (core, jwt, exchange, bypass, identity)
│ └── adapter/ # interface adapters (extauthz, extproc, httpproxy, reverseproxy)
├── cmd/
│ ├── waypoint/ # ext_authz (replaces current cmd/token-exchange-service)
│ ├── authbridge/ # ext_proc (replaces kagenti-extensions go-processor)
│ └── klaviger/ # httpproxy + reverseproxy (replaces grs/klaviger token exchange)
├── deploy/ # existing deploy scripts and YAMLs
└── docs/ # analysis, comparison, research
AuthBridge and Klaviger repos would import the binaries from here rather than maintaining their own token exchange implementations. The webhook/operator in kagenti-extensions continues to handle sidecar injection and Keycloak client registration — this repo only owns the token exchange runtime.
Migration strategy
-
Phase 1: Extract pkg/tokenexchange from current cmd/token-exchange-service/main.go. Build cmd/waypoint that imports it. Verify existing 4 E2E tests pass.
-
Phase 2: Add pkg/adapter/extproc and build cmd/authbridge. Test against AuthBridge's E2E tests.
-
Phase 3: Add pkg/adapter/reverseproxy and build cmd/klaviger. Test against Klaviger's E2E tests.
-
Phase 4: Propose to kagenti-extensions and Klaviger repos to use binaries from this repo instead of their own implementations.
Files
| Path |
Description |
pkg/tokenexchange/core/engine.go |
Core evaluation logic |
pkg/tokenexchange/jwt/provider.go |
JWKS + JWT validation interface + implementations |
pkg/tokenexchange/exchange/client.go |
RFC 8693 client + token cache |
pkg/tokenexchange/exchange/auth.go |
Client authentication methods |
pkg/tokenexchange/bypass/matcher.go |
Bypass path matching |
pkg/tokenexchange/identity/resolver.go |
Agent identity resolution |
pkg/adapter/extauthz/server.go |
ext_authz gRPC adapter |
pkg/adapter/extproc/server.go |
ext_proc gRPC adapter |
pkg/adapter/httpproxy/handler.go |
HTTP forward proxy adapter |
pkg/adapter/reverseproxy/handler.go |
HTTP reverse proxy adapter |
cmd/waypoint/main.go |
Waypoint binary (ext_authz) |
cmd/authbridge/main.go |
AuthBridge binary (ext_proc) |
cmd/klaviger/main.go |
Klaviger binary (httpproxy + reverseproxy) |
Verification
make test — existing 4 E2E tests pass with refactored cmd/waypoint
- Unit tests for each
pkg/tokenexchange/* package
- Integration test: same
engine instance used by ext_authz and httpproxy produces identical decisions
Plan: Modular Token Exchange Library
Context
We have three approaches to token exchange (AuthBridge, Klaviger, Waypoint) that all implement the same core logic independently — JWT validation, RFC 8693 exchange, token caching, bypass paths. The code analysis (
docs/code-analysis.md) shows 80%+ overlap across all three codebases. This refactor extracts the shared logic into a Go library that all three approaches import, with thin adapter layers for each interface and interception mechanism.Architecture
Package Design
pkg/tokenexchange/coreThe single decision function, shared by all adapters:
Requestis adapter-neutral:pkg/tokenexchange/jwtJWKS fetching and JWT validation. Abstracts the differences:
Uses
lestrrat-go/jwx(already used by AuthBridge and Klaviger) instead of manual RSA parsing. Supports RSA + EC keys.pkg/tokenexchange/exchangeRFC 8693 token exchange client:
Includes token cache (SHA-256 keys, TTL, periodic eviction).
pkg/tokenexchange/bypassPath matching (identical in Waypoint and AuthBridge, hardcoded in Klaviger):
pkg/tokenexchange/identityAgent identity resolution for the shared service model:
pkg/adapter/extauthzEnvoy ext_authz gRPC v3 adapter (for waypoint mode):
Translates
CheckRequest→core.Request, callsengine.Evaluate(), translatesAuthDecision→CheckResponse.pkg/adapter/extprocEnvoy ext_proc gRPC v3 adapter (for AuthBridge sidecar mode):
Handles bidirectional streaming. Detects direction from
x-authbridge-directionheader. Inbound → validate. Outbound → exchange.pkg/adapter/httpproxyHTTP forward proxy adapter (for shared proxy and Klaviger outbound):
Receives proxied HTTP requests, calls
engine.Evaluate(), replaces Authorization header, forwards to destination.pkg/adapter/reverseproxyHTTP reverse proxy adapter (for Klaviger inbound and potential shared proxy inbound):
Receives inbound requests, calls
engine.Evaluate()for validation, forwards to backend on localhost.What each binary assembles
cmd/waypoint(current token-exchange-service)cmd/authbridge(go-processor replacement)cmd/klaviger(klaviger replacement)Repo structure
This repo becomes the single home for all token exchange code. Each approach is a
cmd/binary:AuthBridge and Klaviger repos would import the binaries from here rather than maintaining their own token exchange implementations. The webhook/operator in kagenti-extensions continues to handle sidecar injection and Keycloak client registration — this repo only owns the token exchange runtime.
Migration strategy
Phase 1: Extract
pkg/tokenexchangefrom currentcmd/token-exchange-service/main.go. Buildcmd/waypointthat imports it. Verify existing 4 E2E tests pass.Phase 2: Add
pkg/adapter/extprocand buildcmd/authbridge. Test against AuthBridge's E2E tests.Phase 3: Add
pkg/adapter/reverseproxyand buildcmd/klaviger. Test against Klaviger's E2E tests.Phase 4: Propose to kagenti-extensions and Klaviger repos to use binaries from this repo instead of their own implementations.
Files
pkg/tokenexchange/core/engine.gopkg/tokenexchange/jwt/provider.gopkg/tokenexchange/exchange/client.gopkg/tokenexchange/exchange/auth.gopkg/tokenexchange/bypass/matcher.gopkg/tokenexchange/identity/resolver.gopkg/adapter/extauthz/server.gopkg/adapter/extproc/server.gopkg/adapter/httpproxy/handler.gopkg/adapter/reverseproxy/handler.gocmd/waypoint/main.gocmd/authbridge/main.gocmd/klaviger/main.goVerification
make test— existing 4 E2E tests pass with refactoredcmd/waypointpkg/tokenexchange/*packageengineinstance used by ext_authz and httpproxy produces identical decisions