oauthproto

package
v0.25.0 Latest Latest
Warning

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

Go to latest
Published: Apr 28, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package oauthproto provides shared RFC-defined types, constants, and validation utilities for OAuth 2.0 and OpenID Connect. It serves as a shared foundation for both OAuth clients and servers.

Surface area:

  • RFC 8414 authorization server metadata types and well-known paths
  • Redirect URI validation per RFC 6749 and RFC 8252
  • RFC 7591 Dynamic Client Registration client-side types: request/response types, ScopeList JSON codec, RegisterClientDynamically, ToolHiveMCPClientName. The authserver hosts its own server-side DCR types in pkg/authserver/server/registration.
  • Shared constants: UserAgent, well-known paths, grant types, PKCE methods

Leaf-package invariant: this package has no dependency on github.com/stacklok/toolhive/pkg/networking. All callers that need both networking helpers and oauthproto types must import both packages independently.

Index

Constants

View Source
const (
	// WellKnownOIDCPath is the standard OIDC discovery endpoint path
	// per OpenID Connect Discovery 1.0 specification.
	WellKnownOIDCPath = "/.well-known/openid-configuration"

	// WellKnownOAuthServerPath is the standard OAuth authorization server metadata endpoint path
	// per RFC 8414 (OAuth 2.0 Authorization Server Metadata).
	WellKnownOAuthServerPath = "/.well-known/oauth-authorization-server"

	// WellKnownOAuthResourcePath is the RFC 9728 standard path for OAuth Protected Resource metadata.
	// Per RFC 9728 Section 3, this endpoint and any subpaths under it should be accessible
	// without authentication to enable OIDC/OAuth discovery.
	WellKnownOAuthResourcePath = "/.well-known/oauth-protected-resource"
)

Well-known endpoint paths as defined by RFC 8414, OpenID Connect Discovery 1.0, and RFC 9728.

View Source
const (
	// GrantTypeAuthorizationCode is the authorization code grant type (RFC 6749 Section 4.1).
	GrantTypeAuthorizationCode = "authorization_code"

	// GrantTypeRefreshToken is the refresh token grant type (RFC 6749 Section 6).
	GrantTypeRefreshToken = "refresh_token"
)

Grant types as defined by RFC 6749.

View Source
const (
	// TokenTypeAccessToken indicates an OAuth 2.0 access token (RFC 8693 Section 3).
	TokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token"

	// TokenTypeIDToken indicates an OpenID Connect ID Token (RFC 8693 Section 3).
	TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token"

	// TokenTypeJWT indicates a JSON Web Token (RFC 8693 Section 3).
	TokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt"
)

Token type URNs as defined by RFC 8693.

View Source
const (
	// GrantTypeTokenExchange is the OAuth 2.0 Token Exchange grant type (RFC 8693).
	GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange"
)

Grant type URNs for token exchange protocols.

View Source
const MaxRedirectURILength = 2048

MaxRedirectURILength is the maximum allowed length for a single redirect URI. This limit provides DoS protection during URI parsing per RFC 3986 practical constraints.

View Source
const (
	// PKCEMethodS256 uses SHA-256 hash of the code verifier (recommended).
	PKCEMethodS256 = "S256"
)

PKCE (Proof Key for Code Exchange) methods as defined by RFC 7636.

View Source
const (
	// ResponseTypeCode is the authorization code response type (RFC 6749 Section 4.1.1).
	ResponseTypeCode = "code"
)

Response types as defined by RFC 6749.

View Source
const (
	// TokenEndpointAuthMethodNone indicates no client authentication (public clients).
	// Typically used with PKCE for native/mobile applications.
	TokenEndpointAuthMethodNone = "none"
)

Token endpoint authentication methods as defined by RFC 7591.

View Source
const ToolHiveMCPClientName = "ToolHive MCP Client"

ToolHiveMCPClientName is the name advertised in dynamic client registration requests.

View Source
const (
	// UserAgent is the User-Agent header value sent on all HTTP requests
	// originating from this package and its callers.
	UserAgent = "ToolHive/1.0"
)

HTTP client constants.

Variables

View Source
var (
	// ErrMissingIssuer indicates the issuer field is missing from the discovery document.
	ErrMissingIssuer = errors.New("missing issuer")

	// ErrMissingAuthorizationEndpoint indicates the authorization_endpoint field is missing.
	ErrMissingAuthorizationEndpoint = errors.New("missing authorization_endpoint")

	// ErrMissingTokenEndpoint indicates the token_endpoint field is missing.
	ErrMissingTokenEndpoint = errors.New("missing token_endpoint")

	// ErrMissingJWKSURI indicates the jwks_uri field is missing (required for OIDC).
	ErrMissingJWKSURI = errors.New("missing jwks_uri")

	// ErrMissingResponseTypesSupported indicates the response_types_supported field is missing (required for OIDC).
	ErrMissingResponseTypesSupported = errors.New("missing response_types_supported")
)

Validation errors for discovery documents.

Functions

func DefaultHTTPClient

func DefaultHTTPClient() *http.Client

DefaultHTTPClient returns the process-wide *http.Client used by OAuth grant helpers when no explicit client is injected. Callers that build their own requests and call client.Do directly (without going through DoTokenRequest) use this to pick up the shared transport and inherit the same connection-reuse and timeout behavior as the helper path.

The returned client is a process-wide singleton — callers MUST NOT mutate its Timeout or Transport fields. Code wanting custom timeouts must construct its own *http.Client and pass it to DoTokenRequest.

http.Client is documented as safe for concurrent use by multiple goroutines (see https://pkg.go.dev/net/http#Client), so the returned value can be shared across goroutines without additional synchronization.

TODO: consider a future opt-in SSRF-protected variant backed by pkg/networking.NewHttpClientBuilder. The builder blocks loopback and RFC 1918 ranges, which would break localhost IdPs (dex, Keycloak-in-Docker) and the httptest.NewServer-based tests that bind to 127.0.0.1. Not a default today for behavior-compatibility with pkg/auth/tokenexchange.

func IsLoopbackHost

func IsLoopbackHost(host string) bool

IsLoopbackHost reports whether host is a loopback hostname or IP address. pkg/networking wraps this function in its own IsLocalhost to avoid a reverse import dependency from this leaf package into networking.

Recognised forms: "localhost", "localhost:<port>", "127.0.0.1", "127.0.0.1:<port>", "[::1]", "[::1]:<port>".

func NewFormRequest

func NewFormRequest(
	ctx context.Context,
	endpoint string,
	data url.Values,
	clientID, clientSecret string,
) (*http.Request, error)

NewFormRequest builds an HTTP POST request for an OAuth 2.0 token endpoint call. The body is the URL-encoded form in data, with Content-Type and Content-Length set. When both clientID and clientSecret are non-empty, HTTP Basic authentication is attached per RFC 6749 Section 2.3.1; the credentials are URL-encoded before being passed to SetBasicAuth, which is what Go's SetBasicAuth and the OAuth 2.0 spec both require.

Callers own the request's Context — pass the deadline or cancellation signal they want DoTokenRequest to honour.

func Redact

func Redact(value string) string

Redact returns "<empty>" for an empty input and "[REDACTED]" otherwise. Grant-subpackage Config.String() methods use it to keep secrets (client secrets, JWT assertions, refresh tokens) out of logs and error output without each Config reimplementing the empty-vs-nonempty branch.

func ValidateRedirectURI

func ValidateRedirectURI(uri string, policy RedirectURIPolicy) error

ValidateRedirectURI validates a redirect URI per RFC 6749 Section 3.1.2 and RFC 8252. The policy parameter controls whether private-use URI schemes are accepted.

Validation rules applied:

Types

type AuthorizationServerMetadata

type AuthorizationServerMetadata struct {
	// Issuer is the authorization server's issuer identifier (REQUIRED per RFC 8414).
	Issuer string `json:"issuer"`

	// AuthorizationEndpoint is the URL of the authorization endpoint (RECOMMENDED).
	// Note: No omitempty to maintain backward compatibility with existing JSON serialization.
	AuthorizationEndpoint string `json:"authorization_endpoint"`

	// TokenEndpoint is the URL of the token endpoint (RECOMMENDED).
	// Note: No omitempty to maintain backward compatibility with existing JSON serialization.
	TokenEndpoint string `json:"token_endpoint"`

	// JWKSURI is the URL of the JSON Web Key Set document (RECOMMENDED).
	// Note: No omitempty to maintain backward compatibility with existing JSON serialization.
	JWKSURI string `json:"jwks_uri"`

	// RegistrationEndpoint is the URL of the Dynamic Client Registration endpoint (OPTIONAL).
	RegistrationEndpoint string `json:"registration_endpoint,omitempty"`

	// IntrospectionEndpoint is the URL of the token introspection endpoint (OPTIONAL, RFC 7662).
	IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`

	// UserinfoEndpoint is the URL of the UserInfo endpoint (RECOMMENDED per OIDC Discovery, not in RFC 8414).
	// Omitted from JSON when empty to avoid serializing an invalid URL value.
	UserinfoEndpoint string `json:"userinfo_endpoint,omitempty"`

	// ResponseTypesSupported lists the response types supported (RECOMMENDED).
	ResponseTypesSupported []string `json:"response_types_supported,omitempty"`

	// GrantTypesSupported lists the grant types supported (OPTIONAL).
	GrantTypesSupported []string `json:"grant_types_supported,omitempty"`

	// CodeChallengeMethodsSupported lists the PKCE code challenge methods supported (OPTIONAL).
	CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`

	// TokenEndpointAuthMethodsSupported lists the authentication methods supported at the token endpoint (OPTIONAL).
	TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`

	// ScopesSupported lists the OAuth 2.0 scope values supported (RECOMMENDED per RFC 8414).
	// For MCP authorization servers, this typically includes "openid" and "offline_access".
	ScopesSupported []string `json:"scopes_supported,omitempty"`
}

AuthorizationServerMetadata represents the OAuth 2.0 Authorization Server Metadata per RFC 8414. This is the base structure that OIDC Discovery extends.

type DynamicClientRegistrationRequest

type DynamicClientRegistrationRequest struct {
	// Required field according to RFC 7591
	RedirectURIs []string `json:"redirect_uris"`

	// Essential fields for OAuth flow
	ClientName              string    `json:"client_name,omitempty"`
	TokenEndpointAuthMethod string    `json:"token_endpoint_auth_method,omitempty"`
	GrantTypes              []string  `json:"grant_types,omitempty"`
	ResponseTypes           []string  `json:"response_types,omitempty"`
	Scopes                  ScopeList `json:"scope,omitempty"`
}

DynamicClientRegistrationRequest represents the request for dynamic client registration (RFC 7591).

type DynamicClientRegistrationResponse

type DynamicClientRegistrationResponse struct {
	// Required fields
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret,omitempty"` //nolint:gosec // G117: field legitimately holds sensitive data

	// Optional fields that may be returned
	ClientIDIssuedAt        int64  `json:"client_id_issued_at,omitempty"`
	ClientSecretExpiresAt   int64  `json:"client_secret_expires_at,omitempty"`
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`

	// Echo back the essential request fields
	ClientName              string    `json:"client_name,omitempty"`
	RedirectURIs            []string  `json:"redirect_uris,omitempty"`
	TokenEndpointAuthMethod string    `json:"token_endpoint_auth_method,omitempty"`
	GrantTypes              []string  `json:"grant_types,omitempty"`
	ResponseTypes           []string  `json:"response_types,omitempty"`
	Scopes                  ScopeList `json:"scope,omitempty"`
}

DynamicClientRegistrationResponse represents the response from dynamic client registration (RFC 7591).

func RegisterClientDynamically

func RegisterClientDynamically(
	ctx context.Context,
	registrationEndpoint string,
	request *DynamicClientRegistrationRequest,
	client *http.Client,
) (*DynamicClientRegistrationResponse, error)

RegisterClientDynamically performs RFC 7591 Dynamic Client Registration against the given registrationEndpoint.

If client is nil, a default *http.Client with a 30 s timeout, 10 s TLS handshake timeout, and 10 s response-header timeout is used. Pass a non-nil client to supply custom transport settings (e.g., in tests using httptest.NewServer).

type OIDCDiscoveryDocument

type OIDCDiscoveryDocument struct {
	// Embed OAuth 2.0 AS Metadata (RFC 8414) as the base
	AuthorizationServerMetadata

	// SubjectTypesSupported lists the subject identifier types supported (REQUIRED for OIDC).
	SubjectTypesSupported []string `json:"subject_types_supported,omitempty"`

	// IDTokenSigningAlgValuesSupported lists the JWS algorithms supported for ID tokens (REQUIRED for OIDC).
	IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`

	// ClaimsSupported lists the claims that can be returned (RECOMMENDED for OIDC).
	ClaimsSupported []string `json:"claims_supported,omitempty"`
}

OIDCDiscoveryDocument represents the OpenID Connect Discovery 1.0 document. It extends OAuth 2.0 Authorization Server Metadata (RFC 8414) with OIDC-specific fields. This unified type supports both producer (server) and consumer (client) use cases.

func (*OIDCDiscoveryDocument) SupportsGrantType

func (d *OIDCDiscoveryDocument) SupportsGrantType(grantType string) bool

SupportsGrantType returns true if the authorization server supports the given grant type.

func (*OIDCDiscoveryDocument) SupportsPKCE

func (d *OIDCDiscoveryDocument) SupportsPKCE() bool

SupportsPKCE returns true if the authorization server supports PKCE with S256.

func (*OIDCDiscoveryDocument) Validate

func (d *OIDCDiscoveryDocument) Validate(isOIDC bool) error

Validate performs basic validation on the discovery document. It checks for required fields based on whether this is an OIDC or pure OAuth document.

type RedirectURIPolicy

type RedirectURIPolicy int

RedirectURIPolicy controls which URI schemes are accepted during redirect URI validation.

const (
	// RedirectURIPolicyStrict allows only https and http-loopback schemes.
	// This follows RFC 8252 Section 8.4 strict security recommendations and
	// is appropriate for dynamically registered clients where scheme hijacking
	// is a concern.
	RedirectURIPolicyStrict RedirectURIPolicy = iota

	// RedirectURIPolicyAllowPrivateSchemes also allows private-use URI schemes
	// (e.g., cursor://, vscode://) per RFC 8252 Section 7.1.
	// This is appropriate for pre-registered/static clients where the administrator
	// explicitly configures trusted redirect URIs for native applications.
	RedirectURIPolicyAllowPrivateSchemes
)

type ScopeList

type ScopeList []string

ScopeList represents the "scope" field in both dynamic client registration requests and responses.

Marshaling (requests): Per RFC 7591 Section 2, scopes are serialized as a space-delimited string. Examples:

  • []string{"openid", "profile", "email"} → "openid profile email"
  • []string{"openid"} → "openid"
  • nil or []string{} → omitted (via omitempty)

Unmarshaling (responses): Some servers return scopes as a space-delimited string per RFC 7591, while others return a JSON array. This type normalizes both formats into []string. Examples:

  • "openid profile email" → []string{"openid", "profile", "email"}
  • ["openid","profile","email"] → []string{"openid", "profile", "email"}
  • null → nil
  • "" or ["", " "] → nil

func (ScopeList) MarshalJSON

func (s ScopeList) MarshalJSON() ([]byte, error)

MarshalJSON implements custom encoding for ScopeList. It converts the slice of scopes into a space-delimited string as required by RFC 7591 Section 2.

Important: This method does NOT handle empty slices. Go's encoding/json package evaluates omitempty by checking if the Go value is "empty" (len(slice) == 0) BEFORE calling MarshalJSON. Empty slices are omitted at the struct level, so this method is never invoked for empty slices. This means we don't need to return null or handle the empty case - omitempty does it for us automatically.

See: https://pkg.go.dev/encoding/json (omitempty checks zero values before marshaling)

func (*ScopeList) UnmarshalJSON

func (s *ScopeList) UnmarshalJSON(data []byte) error

UnmarshalJSON implements custom decoding for ScopeList. It supports both string and array encodings of the "scope" field, trimming whitespace and normalizing empty values to nil for consistent semantics.

type TokenResponse

type TokenResponse struct {
	// Token carries AccessToken, TokenType, RefreshToken, and Expiry populated
	// from the response body. The Raw and other unexported fields on
	// oauth2.Token are not set — use the sibling fields on TokenResponse.
	Token *oauth2.Token

	// IssuedTokenType is the RFC 8693 issued_token_type URN when the server
	// performed a token exchange. Empty for plain RFC 6749 responses.
	IssuedTokenType string

	// Scope is the raw (space-separated) scope string returned by the server.
	// Callers that need the list form can strings.Fields(scope).
	Scope string
}

TokenResponse is the public result of a successful token endpoint exchange. It composes *oauth2.Token rather than embedding it, so the Token's Valid() / Extra() / Type() helpers are not promoted onto this type and future oauth2.Token additions cannot leak into this package's API.

For the fields that RFC 8693 Section 2.2.1 requires beyond the standard oauth2 token (issued_token_type and scope), callers read them off the response directly.

func DoTokenRequest

func DoTokenRequest(client *http.Client, req *http.Request) (*TokenResponse, error)

DoTokenRequest executes a prepared token endpoint request and returns the parsed response. It is the high-level counterpart to NewFormRequest: most grants call NewFormRequest followed by DoTokenRequest.

Passing a nil client is explicitly supported and selects the package-level shared default (see DefaultHTTPClient). Callers that need custom timeouts, a custom transport, or a test double MUST supply their own *http.Client — the nil shortcut does not take any caller-visible options.

Behavior:

  • If client is nil, DefaultHTTPClient is used so callers automatically get the shared transport (connection reuse, consistent timeouts).
  • The response body is read with io.LimitReader capped at maxResponseBodySize (1 MiB, matching x/oauth2) before any parsing, so a pathological server cannot exhaust memory.
  • On every exit path — success, JSON decode failure, and RetrieveError — the body is closed. The body is deliberately NOT drained: io.Copy(io.Discard, resp.Body) would be unbounded on oversized or never-terminating bodies and would defeat the 1 MiB cap above. When the body exceeds the cap, net/http cannot reuse the connection; that is the intended tradeoff and matches x/oauth2/internal/token.go.
  • RFC 6749 Section 5.2 routing (a 2xx body with an "error" field) is handled inside ParseTokenResponse; DoTokenRequest surfaces the resulting *oauth2.RetrieveError unchanged.

The request's own Context is authoritative: NewFormRequest builds the request with the caller's context attached, and client.Do observes req.Context() for cancellation and deadlines. A cancelled context fails fast without reaching the server.

func ParseTokenResponse

func ParseTokenResponse(resp *http.Response, body []byte) (*TokenResponse, error)

ParseTokenResponse is the single entry point for decoding a token endpoint response. It enforces RFC 6749 Sections 5.1 and 5.2 in a single place:

  • The body is decoded as JSON first, independent of the HTTP status.
  • If the status is non-2xx, OR the body carries an "error" field (RFC 6749 §5.2 — some providers return HTTP 200 with an error payload), an *oauth2.RetrieveError is returned and *TokenResponse is nil.
  • On success, access_token must be non-empty (RFC 6749 §5.1). token_type is intentionally NOT required here: the x/oauth2 library treats it as optional (Token.Type() defaults to "Bearer") and Google historically omits it. Per-grant callers are responsible for any stricter validation their specification demands.

The caller is responsible for grant-specific validation (for example, RFC 8693 Section 2.2.1 requires the issued_token_type field to be present; that check belongs in the token-exchange grant's call site, not here).

Malformed JSON on a failure status still yields an *oauth2.RetrieveError with the raw body preserved, so callers can surface the server's reply verbatim. Malformed JSON on a 2xx status is returned as a wrapped json.SyntaxError / json.UnmarshalTypeError via fmt.Errorf("%w", ...).

Directories

Path Synopsis
Package oauthtest provides shared test fixtures for OAuth 2.0 response decoding.
Package oauthtest provides shared test fixtures for OAuth 2.0 response decoding.

Jump to

Keyboard shortcuts

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