Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions pkg/auth/dcr/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

package dcr

// Request is the profile-neutral input to ResolveCredentials. Each consumer
// (embedded authserver, CLI OAuth flow) translates its domain types into a
// Request at the call site so the resolver does not import any consumer's
// shapes.
//
// The struct collects exactly the fields the resolver reads:
//
// - Identity: Issuer (the caller's logical scope — see the Issuer field
// doc for what each consumer puts there), Scopes, and RedirectURI.
// - Endpoint discovery: DiscoveryURL or RegistrationEndpoint, with optional
// explicit AuthorizationEndpoint / TokenEndpoint overrides.
// - Registration metadata: InitialAccessToken, ClientName. The caller is
// responsible for resolving any file-or-env reference (e.g. the embedded
// authserver's InitialAccessTokenFile / InitialAccessTokenEnvVar) into
// InitialAccessToken before constructing a Request.
//
// Mutually exclusive constraints are enforced at validation time:
//
// - Exactly one of DiscoveryURL / RegistrationEndpoint must be non-empty.
// - Issuer must be non-empty.
//
// Constructing a Request is the caller's responsibility; the resolver does
// not clone or mutate it.
type Request struct {
// Issuer is the caller's logical scope for cache keying and (when
// RedirectURI is empty) the basis for the default redirect URI
// ({Issuer}/oauth/callback). Required.
//
// What each consumer puts here:
//
// - Embedded authserver: its own issuer identifier. The authserver
// owns a distinct logical identity from the upstream IdP, and
// defaulting RedirectURI to {Issuer}/oauth/callback lands the
// callback on the authserver's origin (correct).
// - CLI OAuth flow: the upstream OAuth provider's issuer URL,
// because the CLI has no separate logical issuer of its own. The
// CLI always supplies an explicit loopback RedirectURI per
// RFC 8252 §7.3, so the redirect-URI defaulting is never
// exercised on this path.
//
// The resolver's cache key is (Issuer, RedirectURI, ScopesHash); the
// two consumers do not collide on the key because the embedded
// authserver's RedirectURI lives on the authserver's origin and the
// CLI's lives on a loopback (RFC 8252 §7.3) — even when Issuer
// happens to match between the two profiles, the RedirectURI
// component keeps the cache key distinct. See the cross-consumer
// caveat on dcrFlight in resolver.go for the wider invariant.
//
// Issuer is *not* used for RFC 8414 §3.3 metadata verification —
// that uses an issuer recovered from DiscoveryURL inside the
// resolver.
Issuer string

// RedirectURI is the redirect URI to register with the upstream. When
// empty, the resolver derives {Issuer}/oauth/callback. HTTPS is required
// except for loopback hosts (RFC 8252 §7.3 — the CLI flow uses
// http://localhost:{port}/callback).
RedirectURI string

// Scopes are the OAuth scopes to request in the registration body.
// May be empty; the resolver will fall back to discovered scopes if the
// upstream advertises any.
Scopes []string

// DiscoveryURL points at the upstream's RFC 8414 / OIDC Discovery
// document. The resolver fetches this URL exactly once and reads
// registration_endpoint, authorization_endpoint, token_endpoint,
// token_endpoint_auth_methods_supported, scopes_supported, and
// code_challenge_methods_supported from it.
//
// Mutually exclusive with RegistrationEndpoint.
DiscoveryURL string

// RegistrationEndpoint is used directly when set, bypassing discovery.
// On this branch the caller is expected to also supply AuthorizationEndpoint
// and TokenEndpoint explicitly; the resolver's auth-method selection
// defaults to client_secret_basic because no server-capability fields
// are available.
//
// Mutually exclusive with DiscoveryURL.
RegistrationEndpoint string

// AuthorizationEndpoint, when non-empty, overrides any value discovered
// via DiscoveryURL. Explicit caller configuration always wins.
AuthorizationEndpoint string

// TokenEndpoint, when non-empty, overrides any value discovered via
// DiscoveryURL. Explicit caller configuration always wins.
TokenEndpoint string

// InitialAccessToken is the RFC 7591 initial access token presented to
// the registration endpoint as a Bearer header. The caller resolves any
// file-or-env reference before populating this field.
InitialAccessToken string

// ClientName is sent as the RFC 7591 "client_name" registration
// metadata. When empty, the resolver uses
// oauthproto.ToolHiveMCPClientName.
ClientName string

// PublicClient, when true, instructs the resolver to register as a
// public PKCE client (token_endpoint_auth_method=none) regardless of
// what other methods the upstream advertises. This matches the CLI
// flow's intent (RFC 8252 §8.4 native public clients) and the
// resolver still enforces the RFC 7636 / OAuth 2.1 S256 PKCE gate:
// when the upstream's discovery metadata does not advertise S256 in
// code_challenge_methods_supported, the registration is refused with
// a clear error rather than silently downgrading.
//
// When false (the embedded-authserver default), the resolver picks
// the strongest auth method the upstream advertises (private_key_jwt
// > client_secret_basic > client_secret_post > none, with the same
// S256 gate on "none").
//
// Has no effect on the RegistrationEndpoint-direct branch when the
// caller has not also supplied a DiscoveryURL: without
// code_challenge_methods_supported the S256 gate cannot be evaluated,
// so the resolver refuses to register as a public client.
PublicClient bool
}
Loading
Loading