Skip to content

Modular token exchange library: shared core for AuthBridge, Klaviger, and Waypoint #279

@huang195

Description

@huang195

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 CheckRequestcore.Request, calls engine.Evaluate(), translates AuthDecisionCheckResponse.

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

  1. 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.

  2. Phase 2: Add pkg/adapter/extproc and build cmd/authbridge. Test against AuthBridge's E2E tests.

  3. Phase 3: Add pkg/adapter/reverseproxy and build cmd/klaviger. Test against Klaviger's E2E tests.

  4. 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

  1. make test — existing 4 E2E tests pass with refactored cmd/waypoint
  2. Unit tests for each pkg/tokenexchange/* package
  3. Integration test: same engine instance used by ext_authz and httpproxy produces identical decisions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions