Documentation
¶
Overview ¶
Package runner provides integration between the proxy runner and the auth server.
Index ¶
- Constants
- type DCRCredentialStore
- type DCRKey
- type DCRResolution
- type EmbeddedAuthServer
- func (e *EmbeddedAuthServer) Close() error
- func (e *EmbeddedAuthServer) Handler() http.Handler
- func (e *EmbeddedAuthServer) IDPTokenStorage() storage.UpstreamTokenStorage
- func (e *EmbeddedAuthServer) KeyProvider() keys.KeyProvider
- func (e *EmbeddedAuthServer) RegisterHandlers(mux *http.ServeMux)
- func (e *EmbeddedAuthServer) Routes() map[string]http.Handler
- func (e *EmbeddedAuthServer) UpstreamTokenRefresher() storage.UpstreamTokenRefresher
Constants ¶
const ( // RedisUsernameEnvVar is the environment variable for the Redis ACL username. // #nosec G101 -- This is an environment variable name, not a hardcoded credential RedisUsernameEnvVar = "TOOLHIVE_AUTH_SERVER_REDIS_USERNAME" // RedisPasswordEnvVar is the environment variable for the Redis ACL password. // #nosec G101 -- This is an environment variable name, not a hardcoded credential RedisPasswordEnvVar = "TOOLHIVE_AUTH_SERVER_REDIS_PASSWORD" )
Redis ACL credential environment variable names. These are set by the operator when Redis storage is configured.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DCRCredentialStore ¶ added in v0.27.0
type DCRCredentialStore interface {
// Get returns the cached resolution for key, or (nil, false, nil) if the
// key is not present. An error is returned only on backend failure.
Get(ctx context.Context, key DCRKey) (*DCRResolution, bool, error)
// Put stores the resolution for key, overwriting any existing entry.
// Implementations must reject a nil resolution with an error rather
// than silently succeeding — a no-op would leave callers with no
// debug trail for the subsequent Get miss.
Put(ctx context.Context, key DCRKey, resolution *DCRResolution) error
}
DCRCredentialStore caches RFC 7591 Dynamic Client Registration resolutions keyed by the (Issuer, RedirectURI, ScopesHash) tuple. Implementations must be safe for concurrent use.
The store is an in-memory cache of long-lived registrations — it is not a durable store, and entries are never expired or evicted by the store itself. Callers are responsible for invalidating entries when the underlying registration is revoked (e.g., via RFC 7592 deregistration).
func NewInMemoryDCRCredentialStore ¶ added in v0.27.0
func NewInMemoryDCRCredentialStore() DCRCredentialStore
NewInMemoryDCRCredentialStore returns a thread-safe in-memory DCRCredentialStore intended for tests and single-replica development deployments. Production deployments should use the Redis-backed store introduced in Phase 3, which addresses the cross-replica sharing, durability, and cross-process coordination gaps documented below.
Entries are retained for the process lifetime; there is no TTL and no background cleanup goroutine. The usual concern about an unbounded cache leaking memory does not apply here because the key space is bounded by the operator-configured upstream count, and this implementation is not the production answer.
What this enables: serialises Get/Put against a single in-process map so concurrent callers within one authserver process see a consistent view of the cache without redundant RFC 7591 registrations.
What this does NOT solve:
- Cross-replica sharing: each replica holds its own independent map, so a registration performed on replica A is not visible to replica B. In a multi-replica deployment every replica will register its own DCR client against the upstream on first boot. Phase 3 introduces a Redis-backed store that addresses this.
- Durability across restarts: process exit drops every entry; the next boot re-registers. Operators relying on stable client_ids must use a persistent backend.
- Cross-process write coordination: two processes (or replicas) calling Put for the same DCRKey concurrently will both succeed against their local maps; whichever registration the upstream accepts last wins on that side, the loser becomes orphaned. The resolveDCRCredentials-level singleflight in dcr.go only deduplicates within one process.
type DCRKey ¶ added in v0.27.0
type DCRKey struct {
// Issuer is *this* auth server's issuer identifier — the local issuer
// of the embedded authorization server that performed the registration,
// NOT the upstream's. The cache is keyed by this value because two
// different local issuers registering against the same upstream are
// distinct OAuth clients and must not share credentials. The upstream's
// issuer is used only for RFC 8414 §3.3 metadata verification inside
// the resolver and is not part of the cache key.
Issuer string
// RedirectURI is the redirect URI registered with the upstream
// authorization server. Lives on the local issuer's origin since it is
// where the upstream sends the user back to us after authentication.
RedirectURI string
// ScopesHash is the SHA-256 hex digest of the sorted scope list.
// See scopesHash in dcr.go for the canonical form.
ScopesHash string
}
DCRKey is the canonical lookup key for a DCR resolution. The tuple is designed so a future Redis-backed store can serialise it into a single key segment (Phase 3) without redefining the canonical form. ScopesHash rather than the raw scope slice is used so the key is comparable and order- insensitive.
type DCRResolution ¶ added in v0.27.0
type DCRResolution struct {
// ClientID is the RFC 7591 "client_id" returned by the authorization
// server.
ClientID string
// ClientSecret is the RFC 7591 "client_secret" returned by the
// authorization server. Empty for public PKCE clients.
ClientSecret string
// AuthorizationEndpoint is the discovered (or configured) authorization
// endpoint for this upstream.
AuthorizationEndpoint string
// TokenEndpoint is the discovered (or configured) token endpoint for this
// upstream.
TokenEndpoint string
// RegistrationAccessToken is the RFC 7592 "registration_access_token"
// required for subsequent registration management operations (update,
// read, delete).
RegistrationAccessToken string
// RegistrationClientURI is the RFC 7592 "registration_client_uri" for
// registration management operations.
RegistrationClientURI string
// TokenEndpointAuthMethod is the authentication method negotiated at the
// token endpoint for this client.
TokenEndpointAuthMethod string
// RedirectURI is the redirect URI presented to the authorization server
// during registration. When the caller's run-config did not specify one,
// this holds the defaulted value derived from the issuer + /oauth/callback
// (via resolveUpstreamRedirectURI). Persisting it on the resolution lets
// consumeResolution write it back onto the run-config COPY so that
// downstream consumers (buildPureOAuth2Config, upstream.OAuth2Config
// validation) see a non-empty RedirectURI.
RedirectURI string
// ClientIDIssuedAt is the RFC 7591 §3.2.1 "client_id_issued_at" value
// converted to a Go time.Time. Zero when the upstream omitted the field
// (the field is OPTIONAL per RFC 7591). Informational; not used to
// invalidate the cache.
ClientIDIssuedAt time.Time
// ClientSecretExpiresAt is the RFC 7591 §3.2.1 "client_secret_expires_at"
// value converted to a Go time.Time. The wire convention is that 0 means
// "the secret does not expire"; in this struct that is represented by
// the zero time.Time so callers can use IsZero() rather than special-
// casing 0.
//
// When non-zero, this field is the authoritative signal that
// lookupCachedResolution uses to refetch credentials before the upstream
// rejects them at the token endpoint. The 90-day dcrStaleAgeThreshold
// is a heuristic for "consider rotating"; this is a hard expiry asserted
// by the upstream itself.
ClientSecretExpiresAt time.Time
// CreatedAt is the wall-clock time at which the resolution was completed.
// Used by Step 2g observability to compute staleness against
// dcrStaleAgeThreshold.
CreatedAt time.Time
}
DCRResolution captures the full RFC 7591 + RFC 7592 response for a successful Dynamic Client Registration, together with the endpoints the upstream advertises so the caller need not re-discover them.
The struct is the unit of storage in DCRCredentialStore and the unit of application via consumeResolution.
type EmbeddedAuthServer ¶
type EmbeddedAuthServer struct {
// contains filtered or unexported fields
}
EmbeddedAuthServer wraps the authorization server for integration with the proxy runner. It handles configuration transformation from authserver.RunConfig to authserver.Config, manages resource lifecycle, and provides HTTP handlers for OAuth/OIDC endpoints.
func NewEmbeddedAuthServer ¶
func NewEmbeddedAuthServer(ctx context.Context, cfg *authserver.RunConfig) (*EmbeddedAuthServer, error)
NewEmbeddedAuthServer creates an EmbeddedAuthServer from authserver.RunConfig. It loads signing keys from files, reads HMAC secrets from files, resolves the upstream client secret from file or environment variable, and initializes all auth server components.
The cfg parameter contains file paths and environment variable names that are resolved at runtime to build the underlying authserver.Config.
func (*EmbeddedAuthServer) Close ¶
func (e *EmbeddedAuthServer) Close() error
Close releases resources held by the EmbeddedAuthServer. This method is idempotent - subsequent calls after the first will return the same error (if any) without attempting to close resources again. Should be called during runner shutdown.
func (*EmbeddedAuthServer) Handler ¶
func (e *EmbeddedAuthServer) Handler() http.Handler
Handler returns the HTTP handler for OAuth/OIDC endpoints. The handler uses internal chi routing and serves all endpoints:
- /oauth/authorize, /oauth/callback, /oauth/token, /oauth/register
- /.well-known/jwks.json, /.well-known/oauth-authorization-server, /.well-known/openid-configuration
func (*EmbeddedAuthServer) IDPTokenStorage ¶ added in v0.9.2
func (e *EmbeddedAuthServer) IDPTokenStorage() storage.UpstreamTokenStorage
IDPTokenStorage returns storage for upstream IDP tokens. Returns nil if no upstream IDP is configured. This is used by the upstream swap middleware to exchange ToolHive JWTs for upstream IDP tokens.
func (*EmbeddedAuthServer) KeyProvider ¶ added in v0.15.0
func (e *EmbeddedAuthServer) KeyProvider() keys.KeyProvider
KeyProvider returns the signing key provider used by the authorization server. This enables in-process JWKS key lookups, eliminating the need for self-referential HTTP calls when the token validator runs in the same process.
func (*EmbeddedAuthServer) RegisterHandlers ¶ added in v0.13.0
func (e *EmbeddedAuthServer) RegisterHandlers(mux *http.ServeMux)
RegisterHandlers registers the authorization server's HTTP routes on the given mux.
func (*EmbeddedAuthServer) Routes ¶ added in v0.13.0
func (e *EmbeddedAuthServer) Routes() map[string]http.Handler
Routes returns the authorization server's HTTP route map.
The /.well-known/ paths are registered explicitly because that namespace is shared: the vMCP server owns /.well-known/oauth-protected-resource (RFC 9728) on the same mux. Adding a new AS /.well-known/ endpoint therefore requires an explicit entry here.
Discovery paths are registered with both exact and trailing-slash (prefix) patterns. The trailing-slash variants support RFC 8414 Section 3.1 path-based issuers, where the client constructs /.well-known/oauth-authorization-server/{issuer-path}.
The /oauth/ subtree is registered as a prefix, so new /oauth/* endpoints added to the chi router are picked up automatically without changes to this method.
func (*EmbeddedAuthServer) UpstreamTokenRefresher ¶ added in v0.11.3
func (e *EmbeddedAuthServer) UpstreamTokenRefresher() storage.UpstreamTokenRefresher
UpstreamTokenRefresher returns a refresher that can refresh expired upstream tokens using the upstream provider's refresh token grant.