tokenexchange

package
v1.13.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package tokenexchange ships Harbor's pull-based, non-interactive external-credential acquisition strategy: a driver on the OAuth flow-strategy registry that obtains a downstream tool credential (a user's Microsoft 365 token, a Google Workspace token — foreign-IdP tokens used to call third-party tools) from an operator-configured external credential broker (a fleet orchestrator, an enterprise token vault, an STS) via an RFC-8693-shaped token exchange, instead of Harbor's interactive authorization-code flow.

Why a pull, not a push

A fleet orchestrator holding each user's downstream credentials in ONE central place wants to provide them to whichever runtime's tool call needs them, instead of every runtime independently acquiring and sealing its own copy (N consents, N encrypted copies). Harbor answers with a PULL: the runtime, at token-miss time, presents its own env-indirected broker credential plus the VERIFIED ctx identity triple and receives a short-lived, audience-bound downstream token. A push (a per-run credential arriving in-band over the Protocol) is rejected as credential passthrough — the runtime cannot verify its provenance / audience / subject binding, and it rides channels the codebase is engineered to keep secrets out of.

Never persisted

Brokered tokens are TTL-cached in memory only, single-flight per (scope, tenant, user, source). The shared TokenStore's Put is NEVER called: the broker stays the single source of truth, so revocation at the broker is not defeated by a live sealed copy in N runtimes (the shadow-source-of-truth smell read southbound).

Cache TTL semantics: a token is served from cache until the EARLIER of the broker-advertised expiry and the operator cache-TTL cap (extra.cache_ttl_cap, default 5m) — a token is never served past the broker's own expiry. Separately, RE-EXCHANGE is rate-floored at 30s per key: within 30s of the previous exchange a re-exchange fails loudly (a typed exchange error naming the floor) rather than round-tripping the broker; once the window elapses the next Token() call re-exchanges normally. The floor only ever bites when the broker advertises an expiry shorter than 30s (a broker misconfiguration) — the net effect there is at most one exchange per 30s window with loud failures in between, never one exchange + one audit event per tool call.

Cache granularity: the subject_token presented to the broker carries the full verified (tenant, user, session) triple, but the cache and single-flight key deliberately scope by (binding scope, tenant, user, source) — the user-bound granularity. Two sessions of the same (tenant, user) share the cached credential by design; a broker that scopes grants per-session should advertise a short expiry (the cache never outlives it).

Fail loud, one mode per source

Broker unreachable / 5xx / a non-consent OAuth error → a wrapped ErrExchangeFailed to the run. NEVER a silent fallback to the interactive flow — that would silently void the central-custody policy (N consent prompts reappear). A source is EITHER interactive (`oauth2`) or brokered (`tokenexchange`), declared in config — no dual path. The interactive-flow methods (InitiateFlow / CompleteFlow / DenyFlow) return the typed auth.ErrNonInteractive sentinel.

One pause path

A broker `consent_required`-class refusal surfaces the SAME typed *auth.ErrAuthRequired the interactive flow uses (AuthorizeURL = the broker-supplied consent URL when present), so the run parks on the unified pause/resume primitive. After the user consents centrally, a resume re-drives Token() against the now-granting broker. A bare resume against a still-declining broker simply re-parks.

Trust model, named honestly

V1 is RFC 8693 impersonation semantics: the broker trusts the runtime's client credential to assert the subject triple (subject_token_type = urn:harbor:oauth:token-type:identity-triple, a Harbor-defined URN). The northbound JWT is deliberately NOT forwarded as subject_token — durable runs outlive the initiating request's JWT, so request-token forwarding breaks exactly the durable-run cases Harbor exists for. A signed runtime-side subject assertion is the named post-V1 upgrade path. Every actual exchange emits the canonical tool.credential_exchanged event (zero token bytes).

Credential source

The runtime's OWN broker client credential (`client_id` / `client_secret`) is resolved through the §4.4 credential-source seam (`internal/tools/auth/credsource`), NOT captured at construction. With the default `env` source the credential is resolved once at boot (zero behavior change); with the `remote` source it is pulled from a coordinator endpoint LAZILY at the first exchange — TTL-cached in memory, single-flight, fail-loud. A resolution failure fails the exchange with the typed credsource.ErrCredentialSourceUnavailable; the driver NEVER falls back to env, to an unauthenticated call, or to the interactive flow (§13). A resolved credential with an empty component fails the exchange loud.

Fail-loud at construction

Operator-facing seams demand explicit configuration. The driver fails closed on: a nil CredentialSource (BuildProviders always threads one), empty TokenURL (the broker endpoint — auth_url / redirect_url are interactive-flow fields and are NOT required here), a malformed extra.cache_ttl_cap, and missing deps (Store / Bus / Redactor / Coordinator).

Index

Constants

View Source
const DriverName = "tokenexchange"

DriverName is the canonical name the driver registers under. The `internal/config` validator's `allowedOAuthDrivers` allowlist mirrors this constant.

Variables

View Source
var (
	// ErrMissingCredentialSource — cfg.CredentialSource was nil at
	// construction. BuildProviders always threads one (env or remote);
	// a nil source is a direct-construction bug.
	ErrMissingCredentialSource = errors.New("auth/tokenexchange: CredentialSource is nil (BuildProviders threads the env/remote source; direct callers pass credsource.Static)")
	// ErrMissingClientID — the resolved credential carried an empty
	// client_id. Surfaced at exchange time (the credential resolves
	// through the source seam — at boot for env, lazily for remote).
	ErrMissingClientID = errors.New("auth/tokenexchange: resolved client_id is empty")
	// ErrMissingClientSecret — the resolved credential carried an empty
	// client_secret.
	ErrMissingClientSecret = errors.New("auth/tokenexchange: resolved client_secret is empty")
	// ErrMissingTokenURL — cfg.TokenURL was empty. The broker's
	// RFC-8693 token-exchange endpoint is mandatory; auth_url /
	// redirect_url are interactive-flow fields and are NOT used.
	ErrMissingTokenURL = errors.New("auth/tokenexchange: token_url must not be empty (the credential broker's RFC-8693 token-exchange endpoint)")
	// ErrBadCacheTTLCap — extra.cache_ttl_cap did not parse as a Go
	// duration (e.g. "5m", "90s").
	ErrBadCacheTTLCap = errors.New("auth/tokenexchange: extra.cache_ttl_cap must be a Go duration (e.g. \"5m\", \"90s\")")
	// ErrMissingDeps — a mandatory FactoryDep (Store / Bus / Redactor
	// / Coordinator) was nil.
	ErrMissingDeps = errors.New("auth/tokenexchange: Store / Bus / Redactor / Coordinator are mandatory")
)

Sentinel errors specific to the tokenexchange driver. Broker / upstream failures reuse the parent package's sentinels (auth.ErrExchangeFailed, auth.ErrAuthRequired, auth.ErrIdentityRequired).

Functions

func New

New constructs the `tokenexchange` driver's OAuthProvider for one operator-config entry. Registered as the driver's auth.Factory; the dev stack never calls it directly — auth.Resolve dispatches by name.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL