client

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultASCacheTTL = 1 * time.Hour

DefaultASCacheTTL is the default time-to-live for cached AS metadata entries. AS metadata changes rarely (endpoint rotations, scope additions), so a 1-hour TTL balances freshness against redundant HTTP fetches.

View Source
const DefaultClientAssertionLifetime = 60 * time.Second

DefaultClientAssertionLifetime is the assertion lifetime applied when ClientAssertionConfig.Lifetime is zero. 60 seconds matches the short-lived recommendation in OIDC Core §9.

View Source
const RefreshThreshold = 5 * time.Minute

RefreshThreshold is how long before expiry to proactively refresh

Variables

View Source
var ErrRegistrationUnauthorized = fmt.Errorf("registration management: unauthorized")

ErrRegistrationUnauthorized is returned by GetRegistration (and, in #169 / #170, UpdateRegistration / DeleteRegistration) when the authorization server rejects the registration access token. Per RFC 7592 the server returns 401 for any auth failure — wrong token, missing token, or unknown client_id — so callers cannot use this error to distinguish those cases.

Functions

func FollowRedirects added in v0.0.65

func FollowRedirects(httpClient *http.Client) func(string) error

FollowRedirects returns an OpenBrowser function that performs the OAuth authorization flow by following HTTP redirects instead of opening a browser. This enables headless environments: CI, conformance testing, CLI tools.

The returned function GETs the authorization URL using the provided HTTP client. The authorization server redirects to the loopback callback URI, which the LoginWithBrowser loopback server catches — exactly as a browser would.

Usage:

cred, err := authClient.LoginWithBrowser(client.BrowserLoginConfig{
    ClientID:    "my-cli",
    OpenBrowser: client.FollowRedirects(nil),
})

The httpClient should follow redirects (default http.Client behavior). If nil, a default client is used. For authorization servers that require form-based login (e.g., Keycloak), the httpClient must handle cookie/session management and form POST — FollowRedirects is designed for AS endpoints that auto-approve (test/mock servers) or for pre-authenticated sessions.

See: https://github.com/panyam/oneauth/issues/71

func IsLocalhost added in v0.0.68

func IsLocalhost(rawURL string) bool

IsLocalhost returns true if the URL points to a loopback address (localhost, 127.0.0.1, or ::1). This is used to exempt local development servers from HTTPS enforcement.

See: https://www.rfc-editor.org/rfc/rfc8252#section-8.3

func MintClientAssertion added in v0.0.83

func MintClientAssertion(clientID, audience string, cfg ClientAssertionConfig) (string, error)

MintClientAssertion produces a signed JWT bearing the client's identity, suitable for use as the `client_assertion` form parameter at the token / introspection / revocation endpoints.

When cfg.Audience is non-empty it overrides the positional audience argument — use the field to target RFC 7523bis ASes that require `aud == issuer URL`; rely on the positional default (token endpoint URL) for OIDC Core §9-compliant ASes.

Claims set per RFC 7523 §3 + OIDC Core §9:

iss = clientID
sub = clientID
aud = cfg.Audience if set, else `audience` argument
jti = random 128-bit hex string
iat = now
exp = now + cfg.Lifetime  (or default)

Returns the compact JWS string ready to drop into a form value.

func ValidateCIMDURL added in v0.0.68

func ValidateCIMDURL(rawURL string) error

ValidateCIMDURL validates a Client ID Metadata Document URL. Per draft-ietf-oauth-client-id-metadata-document:

  • MUST use HTTPS (except localhost for development/testing)
  • MUST contain a non-root path (the URL must identify a specific document)

See: https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html

func ValidateHTTPS added in v0.0.68

func ValidateHTTPS(meta *ASMetadata) error

ValidateHTTPS checks that Authorization Server endpoints use HTTPS. Localhost URLs are exempt for development and testing. Returns nil if metadata is nil (nothing to validate).

See: https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.1

Types

type ASMetadata added in v0.0.56

type ASMetadata struct {
	// Required
	Issuer        string `json:"issuer"`
	TokenEndpoint string `json:"token_endpoint"`

	// Recommended
	AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
	JWKSURI               string `json:"jwks_uri,omitempty"`

	// Optional
	RegistrationEndpoint  string `json:"registration_endpoint,omitempty"`
	IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
	RevocationEndpoint    string `json:"revocation_endpoint,omitempty"`
	UserinfoEndpoint      string `json:"userinfo_endpoint,omitempty"`

	// Supported features
	ScopesSupported                            []string `json:"scopes_supported,omitempty"`
	ResponseTypesSupported                     []string `json:"response_types_supported,omitempty"`
	GrantTypesSupported                        []string `json:"grant_types_supported,omitempty"`
	CodeChallengeMethodsSupported              []string `json:"code_challenge_methods_supported,omitempty"`
	TokenEndpointAuthMethods                   []string `json:"token_endpoint_auth_methods_supported,omitempty"`
	TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
}

ASMetadata holds OAuth 2.0 Authorization Server metadata discovered from a well-known endpoint. Fields follow RFC 8414 and OpenID Connect Discovery 1.0.

See: https://www.rfc-editor.org/rfc/rfc8414#section-2

func DiscoverAS added in v0.0.56

func DiscoverAS(issuerURL string, opts ...DiscoveryOption) (*ASMetadata, error)

DiscoverAS fetches OAuth Authorization Server metadata from well-known endpoints.

It tries the following URLs in order (per RFC 8414 + OIDC Discovery):

For issuer "https://auth.example.com" (no path):

  1. https://auth.example.com/.well-known/oauth-authorization-server
  2. https://auth.example.com/.well-known/openid-configuration

For issuer "https://auth.example.com/tenant1" (with path):

  1. https://auth.example.com/.well-known/oauth-authorization-server/tenant1
  2. https://auth.example.com/tenant1/.well-known/openid-configuration

Returns the first successful response. Returns an error if all attempts fail.

See: https://www.rfc-editor.org/rfc/rfc8414#section-3

type ASMetadataStore added in v0.0.69

type ASMetadataStore interface {
	// Get returns the cached metadata for an issuer if present and not expired.
	// Returns (nil, false) on cache miss or expiry.
	Get(issuer string) (*ASMetadata, bool)

	// Put stores metadata for an issuer with the given TTL. A TTL of 0 means
	// use the store's default. Put replaces any existing entry for the issuer.
	Put(issuer string, md *ASMetadata, ttl time.Duration)
}

ASMetadataStore caches OAuth Authorization Server metadata by issuer URL. Implementations must be safe for concurrent use by multiple goroutines.

A store is typically shared across multiple [OAuthTokenSource] instances in the same process, so that N resource servers sharing one AS only trigger one discovery fetch instead of N.

The store is a hot-path optimization: discovery is cheap (one HTTP fetch) but redundant across concurrent token sources. Callers opt in by passing a store to WithASMetadataStore.

type AuthClient

type AuthClient struct {

	// OnToken is an optional callback invoked after a successful token
	// refresh (the refresh_token grant path through refreshTokenLocked).
	// It fires AFTER the new credential has been stored via CredentialStore,
	// so consumers can use the callback for side-effects (logging, metrics,
	// external persistence) that should observe the post-refresh state.
	//
	// Thread safety: the callback is invoked synchronously from whichever
	// goroutine triggered the refresh. Implementations must be thread-safe
	// if the AuthClient is shared across goroutines.
	//
	// Lock contract: the callback runs while the AuthClient internal mutex
	// is held — same as CredentialStore.SetCredential. Callbacks must NOT
	// re-enter AuthClient methods (GetToken, GetCredential, Login,
	// refreshTokenLocked) or they will deadlock. Callbacks should be
	// lightweight and non-blocking.
	//
	// Does NOT fire for initial logins (Login, LoginWithBrowser) — those
	// return the credential directly to the caller, who can persist it
	// explicitly. Only the automatic refresh_token grant path fires this.
	OnToken func(*ServerCredential)
	// contains filtered or unexported fields
}

AuthClient is an HTTP client with automatic token management

func NewAuthClient

func NewAuthClient(serverURL string, store CredentialStore, opts ...ClientOption) *AuthClient

NewAuthClient creates a new authenticated HTTP client for a server. If store is nil, a no-op store is used — methods that return credentials (Login, LoginWithBrowser, ClientCredentialsToken) still work and return the credential to the caller, but tokens are not persisted between calls.

func (*AuthClient) ClientCredentialsToken added in v0.0.56

func (c *AuthClient) ClientCredentialsToken(clientID, clientSecret string, scopes []string) (*ServerCredential, error)

ClientCredentialsToken authenticates using the client_credentials grant (RFC 6749 §4.4). This is for machine-to-machine authentication where there is no user context — the client authenticates on its own behalf using its client_id and client_secret. No refresh token is issued.

The request is sent as application/x-www-form-urlencoded (RFC 6749 §4.4.2). Client credentials are sent using the negotiated auth method:

  • If AS metadata was provided via WithASMetadata, SelectAuthMethod picks the best method from token_endpoint_auth_methods_supported
  • Without metadata, defaults to client_secret_basic (RFC 6749 §2.3.1)

The resulting access token is stored in the credential store for use by subsequent API calls via the AuthClient's HTTP transport.

See: https://www.rfc-editor.org/rfc/rfc6749#section-4.4 See: https://github.com/panyam/oneauth/issues/72

func (*AuthClient) ClientCredentialsTokenWithAssertion added in v0.0.83

func (c *AuthClient) ClientCredentialsTokenWithAssertion(clientID string, cfg ClientAssertionConfig, scopes []string) (*ServerCredential, error)

ClientCredentialsTokenWithAssertion is the private_key_jwt variant of ClientCredentialsToken (RFC 6749 §4.4 with RFC 7521 §4.2 / RFC 7523 §2.2 client authentication). The client signs a fresh JWT for every request and presents it as `client_assertion` instead of a shared secret. Use this when the AS has registered the client's public key (typically via DCR with token_endpoint_auth_method=private_key_jwt).

The audience claim of the assertion is the discovered token endpoint URL when AS metadata is cached; otherwise the configured token endpoint. Per OIDC Core §9, the AS SHOULD accept this value as an audience identifier — OneAuth's server-side authenticator accepts either the token endpoint URL or the AS issuer.

See: https://www.rfc-editor.org/rfc/rfc7523#section-2.2 See: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

func (*AuthClient) GetCredential

func (c *AuthClient) GetCredential() (*ServerCredential, error)

GetCredential returns the stored credential for this server

func (*AuthClient) GetToken

func (c *AuthClient) GetToken() (string, error)

GetToken returns the current access token, refreshing if needed

func (*AuthClient) HTTPClient

func (c *AuthClient) HTTPClient() *http.Client

HTTPClient returns the underlying HTTP client with auth handling

func (*AuthClient) IsLoggedIn

func (c *AuthClient) IsLoggedIn() bool

IsLoggedIn returns true if there is a valid (non-expired) credential

func (*AuthClient) Login

func (c *AuthClient) Login(username, password, scope string) (*ServerCredential, error)

Login authenticates with username/password and stores the credential

func (*AuthClient) LoginWithBrowser added in v0.0.56

func (c *AuthClient) LoginWithBrowser(cfg BrowserLoginConfig) (*ServerCredential, error)

LoginWithBrowser performs an OAuth 2.0 authorization code flow with PKCE for CLI/headless clients (RFC 8252). It:

  1. Generates PKCE verifier + challenge
  2. Generates a random state parameter for CSRF protection
  3. Starts a temporary loopback HTTP server to catch the redirect
  4. Opens the user's browser to the authorization URL
  5. Waits for the callback with the authorization code
  6. Validates the state parameter
  7. Exchanges the code for tokens using the code_verifier
  8. Stores the credential via the AuthClient's CredentialStore

See: https://www.rfc-editor.org/rfc/rfc8252 (OAuth 2.0 for Native Apps) See: https://www.rfc-editor.org/rfc/rfc7636 (PKCE)

func (*AuthClient) Logout

func (c *AuthClient) Logout() error

Logout removes the credential for this server

func (*AuthClient) ServerURL

func (c *AuthClient) ServerURL() string

ServerURL returns the server URL this client is configured for

type AuthTransport

type AuthTransport struct {
	Base  http.RoundTripper
	Token string
}

AuthTransport wraps an http.RoundTripper to add Authorization headers

func NewAuthTransport

func NewAuthTransport(token string) *AuthTransport

NewAuthTransport creates an AuthTransport with the given token

func NewAuthTransportWithBase

func NewAuthTransportWithBase(base http.RoundTripper, token string) *AuthTransport

NewAuthTransportWithBase creates an AuthTransport with a custom base transport

func (*AuthTransport) RoundTrip

func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper

type BrowserLoginConfig added in v0.0.56

type BrowserLoginConfig struct {
	// AuthorizationEndpoint is the AS authorization URL.
	// If empty, auto-discovered via DiscoverAS(serverURL) using the
	// AuthClient's server URL.
	AuthorizationEndpoint string

	// TokenEndpoint is the AS token URL for code exchange.
	// If empty, uses the AuthClient's configured tokenEndpoint.
	TokenEndpoint string

	// ClientID identifies this client to the authorization server.
	// Required.
	ClientID string

	// Scopes to request (e.g., []string{"openid", "read", "write"}).
	Scopes []string

	// CallbackPort is the port for the loopback redirect server.
	// If 0, a random available port is chosen.
	CallbackPort int

	// Timeout for the entire flow (waiting for user to complete browser login).
	// Defaults to 5 minutes.
	Timeout time.Duration

	// OpenBrowser is called to open the authorization URL in the user's browser.
	// If nil, uses the platform default (open/xdg-open/start).
	// Set to a custom function for testing or headless environments.
	OpenBrowser func(url string) error

	// HTTPClient is used for the token exchange request.
	// If nil, uses http.DefaultClient.
	HTTPClient *http.Client

	// Resource is the RFC 8707 resource indicator — the canonical URI of the
	// target resource server (e.g., "https://api.example.com"). When set, it's
	// included in both the authorization request and token exchange to bind the
	// token to a specific audience. MCP spec requires this parameter.
	//
	// See: https://www.rfc-editor.org/rfc/rfc8707
	Resource string

	// ClientSecret is the client's secret for confidential clients.
	// If empty, the client is treated as a public client (auth method "none")
	// and only client_id is sent in the token exchange.
	// When set, the auth method is negotiated based on AS metadata
	// (token_endpoint_auth_methods_supported) per RFC 6749 §2.3.
	//
	// See: https://github.com/panyam/oneauth/issues/72
	ClientSecret string

	// TokenEndpointAuthMethods specifies the auth methods supported by the AS
	// token endpoint (e.g., ["client_secret_post", "client_secret_basic"]).
	// This is used when AuthorizationEndpoint and TokenEndpoint are explicitly
	// provided and discovery is skipped — without this field, the client
	// defaults to client_secret_basic which may not be what the AS supports.
	//
	// Callers that perform their own AS discovery (e.g., via PRM → AS metadata)
	// should pass token_endpoint_auth_methods_supported here so the client
	// negotiates the correct auth method per RFC 6749 §2.3.
	//
	// If empty and discovery is skipped, defaults to client_secret_basic
	// per RFC 6749 §2.3.1.
	//
	// See: https://www.rfc-editor.org/rfc/rfc6749#section-2.3
	// See: https://github.com/panyam/oneauth/issues/74
	TokenEndpointAuthMethods []string

	// ClientAssertion, when non-nil, switches the code-exchange step to
	// private_key_jwt client authentication (RFC 7521 §4.2 / RFC 7523
	// §2.2 / OIDC Core §9). The client signs a fresh JWT with the
	// configured private key and presents it as `client_assertion`
	// instead of using ClientSecret. Mutually exclusive with
	// ClientSecret — when both are set, ClientAssertion wins.
	ClientAssertion *ClientAssertionConfig
}

BrowserLoginConfig configures the authorization code + PKCE flow for CLI and headless clients.

type ClientAssertionConfig added in v0.0.83

type ClientAssertionConfig struct {
	// PrivateKey is the asymmetric signing key — *rsa.PrivateKey or
	// *ecdsa.PrivateKey. Must match the public key registered with the
	// AS via DCR (RFC 7591) for the same `client_id`.
	PrivateKey crypto.PrivateKey

	// SigningAlg is the JWS alg value: "RS256" or "ES256". Must match
	// the alg the AS registered for this client.
	SigningAlg string

	// KeyID, when non-empty, is set as the `kid` JWS header so the AS
	// can disambiguate among multiple registered keys for the same
	// client. Optional — clients with a single key may leave it blank.
	KeyID string

	// Lifetime is how long the minted assertion is valid for. Defaults
	// to 60s (recommended ceiling for FAPI / most IdPs). Must be ≤
	// 5min to satisfy OneAuth's server-side assertion lifetime cap;
	// other AS implementations are typically stricter.
	Lifetime time.Duration

	// Audience, when non-empty, overrides the `aud` claim of the minted
	// assertion. Empty falls back to the positional `audience` argument
	// of MintClientAssertion (typically the token endpoint URL).
	//
	// RFC 7523bis (current OAuth WG draft) requires `aud` to be the AS
	// issuer identifier; OIDC Core §9 historically permitted the token
	// endpoint URL. ASes in the wild differ — set this when targeting a
	// 7523bis-strict AS that rejects the token endpoint URL.
	Audience string
}

ClientAssertionConfig carries the material a client needs to mint a `private_key_jwt` assertion. Construct once per AuthClient and reuse; each MintClientAssertion call generates a fresh `jti`, `iat`, and `exp` so assertions are single-use.

type ClientCredentialsSource added in v0.0.68

type ClientCredentialsSource struct {
	// TokenEndpoint is the authorization server's token URL.
	TokenEndpoint string

	// ClientID identifies this client to the authorization server.
	ClientID string

	// ClientSecret authenticates this client when ClientAssertion is nil.
	// Sent via the negotiated client_secret_basic or client_secret_post
	// method (RFC 6749 §2.3.1).
	ClientSecret string

	// ClientAssertion, when non-nil, switches the source to the
	// private_key_jwt client-authentication path (RFC 7521 §4.2 /
	// RFC 7523 §2.2 / OIDC Core §9). ClientSecret is ignored. The same
	// caching, OnToken, and ProactiveRefresher machinery applies.
	ClientAssertion *ClientAssertionConfig

	// Scopes to request.
	Scopes []string

	// Refresher enables proactive token refresh before expiry. When nil or
	// Refresher.Buffer == 0, refresh is reactive — happens on the next
	// Token() call after expiry.
	//
	// Typical values: Buffer of 30s-60s for tokens with 5-15 minute lifetimes.
	// The goroutine starts lazily on the first Token() call and runs until
	// Close() is called on the source.
	Refresher *ProactiveRefresher

	// OnToken is an optional callback invoked after every successful token
	// acquisition — both the initial fetch and all subsequent refreshes
	// (reactive and proactive). Use this to persist the credential to an
	// external store (file, database) without implementing a full
	// CredentialStore.
	//
	// Thread safety: the callback may be invoked from the caller's
	// goroutine (initial/reactive path) or from the background refresh
	// goroutine (proactive path). Implementations must be thread-safe.
	//
	// The callback holds no locks of the source, so calling back into
	// Token() from within the callback is safe (but useless — it will
	// return the same token that was just passed in).
	//
	// The credential passed to the callback is the same value returned
	// from AuthClient.ClientCredentialsToken. Mutating it from within the
	// callback is not safe.
	OnToken func(*ServerCredential)
	// contains filtered or unexported fields
}

ClientCredentialsSource implements TokenSource for machine-to-machine auth using the OAuth 2.0 client_credentials grant (RFC 6749 §4.4).

It caches the access token and automatically refreshes it when expired. TokenForScopes supports scope step-up by merging additional scopes and invalidating the cache.

See: https://www.rfc-editor.org/rfc/rfc6749#section-4.4

func (*ClientCredentialsSource) Close added in v0.0.69

func (s *ClientCredentialsSource) Close() error

Close stops the background refresh goroutine if one is running. Safe to call multiple times. Returns nil always (io.Closer compliance). After Close, subsequent Token() calls still work reactively.

func (*ClientCredentialsSource) Token added in v0.0.68

func (s *ClientCredentialsSource) Token() (string, error)

Token returns a cached access token if still valid, or fetches a new one via the client_credentials grant.

If Refresher.Buffer > 0, the background refresh goroutine starts lazily on the first Token() call.

Invokes OnToken (if set) after a successful fetch, outside the source's mutex — callbacks are free to call back into Token() without deadlock.

func (*ClientCredentialsSource) TokenForScopes added in v0.0.68

func (s *ClientCredentialsSource) TokenForScopes(scopes []string) (string, error)

TokenForScopes invalidates the cached token, merges the requested scopes with the existing scope set (using core.UnionScopes), and fetches a fresh token with the combined scopes.

type ClientOption

type ClientOption func(*AuthClient)

ClientOption configures an AuthClient

func WithASMetadata added in v0.0.65

func WithASMetadata(meta *ASMetadata) ClientOption

WithASMetadata pre-populates AS discovery metadata, enabling auth method negotiation in ClientCredentialsToken without a separate discovery request. Useful when DiscoverAS has already been called or for testing.

See: https://github.com/panyam/oneauth/issues/72

func WithHTTPClient

func WithHTTPClient(client *http.Client) ClientOption

WithHTTPClient sets a custom base HTTP client (for timeouts, TLS config, etc.) The transport from this client will be wrapped with auth handling.

func WithTokenEndpoint

func WithTokenEndpoint(path string) ClientOption

WithTokenEndpoint sets a custom token endpoint path

func WithTransport

func WithTransport(transport http.RoundTripper) ClientOption

WithTransport sets a custom base transport (for connection pooling, proxies, etc.)

type ClientRegistrationRequest added in v0.0.68

type ClientRegistrationRequest struct {
	ClientID                string   `json:"client_id,omitempty"`
	ClientName              string   `json:"client_name"`
	ClientURI               string   `json:"client_uri,omitempty"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	ResponseTypes           []string `json:"response_types,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
	Scope                   string   `json:"scope,omitempty"`
	// ApplicationType identifies the client class per OpenID Connect Dynamic
	// Client Registration 1.0 §2: "native" for installed/CLI/SDK clients,
	// "web" for web-server-hosted clients. omitempty so existing callers stay
	// wire-compatible; consumers whose spec requires it (e.g. MCP per SEP-837)
	// set it explicitly.
	ApplicationType string `json:"application_type,omitempty"`
}

ClientRegistrationRequest is the payload for RFC 7591 Dynamic Client Registration and the RFC 7592 §2.2 update body. This represents a client requesting registration (or replacing its registration) at an authorization server's endpoint.

On register the ClientID field is unused (the server assigns the value). On update the ClientID MUST equal the client's existing identifier — the server returns 400 otherwise. UpdateRegistration auto-fills it for callers.

See: https://www.rfc-editor.org/rfc/rfc7591#section-2 See: https://www.rfc-editor.org/rfc/rfc7592#section-2.2

type ClientRegistrationResponse added in v0.0.68

type ClientRegistrationResponse struct {
	ClientID                string   `json:"client_id"`
	ClientSecret            string   `json:"client_secret,omitempty"`
	ClientIDIssuedAt        int64    `json:"client_id_issued_at,omitempty"`
	ClientSecretExpiresAt   int64    `json:"client_secret_expires_at,omitempty"`
	ClientName              string   `json:"client_name,omitempty"`
	ClientURI               string   `json:"client_uri,omitempty"`
	RedirectURIs            []string `json:"redirect_uris,omitempty"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
	Scope                   string   `json:"scope,omitempty"`

	// RFC 7592 §3 — management credentials. RegistrationClientURI is the
	// management endpoint for this specific registration; RegistrationAccessToken
	// is the Bearer token that authorizes calls against it.
	RegistrationAccessToken string `json:"registration_access_token,omitempty"`
	RegistrationClientURI   string `json:"registration_client_uri,omitempty"`
}

ClientRegistrationResponse is the parsed response from a DCR endpoint, extended with the RFC 7592 §3 management credentials so callers can subsequently use GetRegistration / UpdateRegistration / DeleteRegistration against the issuer's management endpoint.

See: https://www.rfc-editor.org/rfc/rfc7591#section-3.2.1 See: https://www.rfc-editor.org/rfc/rfc7592#section-3

func RegisterClient added in v0.0.68

func RegisterClient(endpoint string, meta ClientRegistrationRequest, httpClient *http.Client) (*ClientRegistrationResponse, error)

RegisterClient performs RFC 7591 Dynamic Client Registration against the given endpoint. Returns the assigned client_id and optional client_secret.

If httpClient is nil, http.DefaultClient is used.

See: https://www.rfc-editor.org/rfc/rfc7591#section-3

type CredentialStore

type CredentialStore interface {
	// GetCredential retrieves a credential for a server URL
	// Returns nil, nil if no credential exists for the server
	GetCredential(serverURL string) (*ServerCredential, error)

	// SetCredential stores a credential for a server URL
	SetCredential(serverURL string, cred *ServerCredential) error

	// RemoveCredential removes a credential for a server URL
	RemoveCredential(serverURL string) error

	// ListServers returns all server URLs with stored credentials
	ListServers() ([]string, error)

	// Save persists any pending changes (for stores that batch writes)
	Save() error
}

CredentialStore defines the interface for storing and retrieving credentials

type DeleteRegistrationRequest added in v0.0.81

type DeleteRegistrationRequest struct {
	RegistrationClientURI   string
	RegistrationAccessToken string
	// HTTPClient — see GetRegistrationRequest for rationale.
	HTTPClient *http.Client
}

DeleteRegistrationRequest is the input to DeleteRegistration.

type DeleteRegistrationResponse added in v0.0.81

type DeleteRegistrationResponse struct{}

DeleteRegistrationResponse is intentionally empty today: the AS returns 204 No Content with no body. Wrapped struct preserves the convention's (ctx, *Req) → (*Resp, error) shape and gives forward-compat headroom.

func DeleteRegistration added in v0.0.81

DeleteRegistration performs an RFC 7592 §2.3 deletion. After it returns successfully the AS has removed the registration and invalidated the signing credentials, so any tokens already issued under this client_id will fail subsequent validation.

Maps server responses:

  • 204 No Content → nil error + empty response struct
  • 401 → ErrRegistrationUnauthorized
  • others → generic error including status + body

See: https://www.rfc-editor.org/rfc/rfc7592#section-2.3

type DiscoveryOption added in v0.0.56

type DiscoveryOption func(*discoveryConfig)

DiscoveryOption configures the discovery request.

func WithASCacheTTL added in v0.0.69

func WithASCacheTTL(ttl time.Duration) DiscoveryOption

WithASCacheTTL sets a custom TTL for cache writes during discovery. Only applies when a store is also provided via WithASMetadataStore. A TTL of 0 uses the store's default TTL.

func WithASMetadataStore added in v0.0.69

func WithASMetadataStore(store ASMetadataStore) DiscoveryOption

WithASMetadataStore enables caching of AS metadata via the given store. When set, DiscoverAS checks the store first and returns the cached value on hit. On miss, it fetches from the well-known endpoint and stores the result with the configured TTL (or the store's default).

Typical usage is to share a single store across multiple token sources in the same process:

cache := client.NewMemoryASMetadataStore(0)
md1, _ := client.DiscoverAS("https://auth.example.com", client.WithASMetadataStore(cache))
md2, _ := client.DiscoverAS("https://auth.example.com", client.WithASMetadataStore(cache))
// md2 returned from cache; no second HTTP fetch.

func WithHTTPClientForDiscovery added in v0.0.56

func WithHTTPClientForDiscovery(client *http.Client) DiscoveryOption

WithHTTPClientForDiscovery sets a custom HTTP client for the discovery request. Useful for testing (httptest) and custom TLS configuration.

type GetRegistrationRequest added in v0.0.81

type GetRegistrationRequest struct {
	// RegistrationClientURI is the management endpoint returned at
	// registration time (RFC 7592 §3 registration_client_uri).
	RegistrationClientURI string
	// RegistrationAccessToken authorizes calls to RegistrationClientURI
	// (RFC 7592 §3).
	RegistrationAccessToken string
	// HTTPClient is used to make the request. nil → http.DefaultClient.
	// Conceptually this is a transport option (analog of gRPC CallOption);
	// it lives on the request struct so the method retains a strict
	// (ctx, req) → (resp, err) signature.
	HTTPClient *http.Client
}

GetRegistrationRequest is the input to GetRegistration.

type GetRegistrationResponse added in v0.0.81

type GetRegistrationResponse struct {
	Registration *ClientRegistrationResponse
}

GetRegistrationResponse wraps the parsed registration metadata. Wrapped (rather than returning *ClientRegistrationResponse directly) for symmetry with the server-side ClientRegistrationManager interface and so future fields can be added without changing the method signature.

func GetRegistration added in v0.0.81

GetRegistration performs an RFC 7592 §2.1 read of a previously-registered client.

The server is required to return 401 for any authentication failure; this function maps that case to ErrRegistrationUnauthorized so callers can branch on errors.Is. Other non-2xx responses surface as a generic error including status code and body for diagnostics.

See: https://www.rfc-editor.org/rfc/rfc7592#section-2.1

type MemoryASMetadataStore added in v0.0.69

type MemoryASMetadataStore struct {
	// contains filtered or unexported fields
}

MemoryASMetadataStore is an in-memory ASMetadataStore backed by a sync.RWMutex-protected map. Suitable for single-process deployments where all token sources run in the same binary.

Entries expire lazily on Get — there is no background eviction goroutine. Expired entries stay in the map until a Get or Put touches them, which is fine for bounded-size workloads (one entry per unique issuer URL).

func NewMemoryASMetadataStore added in v0.0.69

func NewMemoryASMetadataStore(defaultTTL time.Duration) *MemoryASMetadataStore

NewMemoryASMetadataStore creates a new in-memory AS metadata store. If defaultTTL is 0, DefaultASCacheTTL is used.

func (*MemoryASMetadataStore) Get added in v0.0.69

func (s *MemoryASMetadataStore) Get(issuer string) (*ASMetadata, bool)

Get returns cached metadata for an issuer if present and not expired. Expired entries are removed lazily on access.

func (*MemoryASMetadataStore) Put added in v0.0.69

func (s *MemoryASMetadataStore) Put(issuer string, md *ASMetadata, ttl time.Duration)

Put stores metadata for an issuer with the given TTL. If ttl is 0, the store's default TTL is used.

type OAuth2TokenRequest

type OAuth2TokenRequest struct {
	GrantType    string `json:"grant_type"`
	Username     string `json:"username,omitempty"`
	Password     string `json:"password,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
	Scope        string `json:"scope,omitempty"`
	ClientID     string `json:"client_id,omitempty"`
	ClientSecret string `json:"client_secret,omitempty"`
	Code         string `json:"code,omitempty"`          // For authorization_code grant
	CodeVerifier string `json:"code_verifier,omitempty"` // PKCE verifier for authorization_code grant
	RedirectURI  string `json:"redirect_uri,omitempty"`  // Redirect URI for authorization_code grant
}

OAuth2TokenRequest is the request body for token endpoint

type OAuth2TokenResponse

type OAuth2TokenResponse struct {
	AccessToken          string `json:"access_token"`
	TokenType            string `json:"token_type"`
	ExpiresIn            int64  `json:"expires_in"`
	RefreshToken         string `json:"refresh_token,omitempty"`
	Scope                string `json:"scope,omitempty"`
	AuthorizationDetails []any  `json:"authorization_details,omitempty"` // RFC 9396 (raw JSON)
	Error                string `json:"error,omitempty"`
	ErrorDesc            string `json:"error_description,omitempty"`
}

OAuth2TokenResponse is the response from token endpoint

type ProactiveRefresher added in v0.0.69

type ProactiveRefresher struct {
	// Buffer is how long before token expiry to refresh. Must be positive
	// to enable proactive refresh. A buffer of 30s means the refresh fires
	// 30 seconds before the token would have expired reactively.
	Buffer time.Duration
	// contains filtered or unexported fields
}

ProactiveRefresher configures and manages background token refresh before expiry. It bundles the refresh policy (Buffer) with the runtime state needed to coordinate the background goroutine lifecycle.

type ScopeAwareTokenSource added in v0.0.68

type ScopeAwareTokenSource interface {
	TokenSource
	TokenForScopes(scopes []string) (string, error)
}

ScopeAwareTokenSource extends TokenSource with scope step-up capability. When additional scopes are required, the cached token is invalidated and a new token is obtained with the merged scope set.

type ServerCredential

type ServerCredential struct {
	AccessToken          string                     `json:"access_token"`
	RefreshToken         string                     `json:"refresh_token,omitempty"`
	TokenType            string                     `json:"token_type,omitempty"`
	UserID               string                     `json:"user_id,omitempty"`
	UserEmail            string                     `json:"user_email,omitempty"`
	Scope                string                     `json:"scope,omitempty"`
	AuthorizationDetails []core.AuthorizationDetail `json:"authorization_details,omitempty"` // RFC 9396
	ExpiresAt            time.Time                  `json:"expires_at"`
	CreatedAt            time.Time                  `json:"created_at"`
}

ServerCredential holds authentication info for a single server

func (*ServerCredential) HasRefreshToken

func (c *ServerCredential) HasRefreshToken() bool

HasRefreshToken returns true if a refresh token is available

func (*ServerCredential) IsExpired

func (c *ServerCredential) IsExpired() bool

IsExpired returns true if the access token has expired

func (*ServerCredential) IsExpiringSoon

func (c *ServerCredential) IsExpiringSoon(within time.Duration) bool

IsExpiringSoon returns true if the token expires within the given duration

type TokenEndpointAuthMethod added in v0.0.65

type TokenEndpointAuthMethod string

TokenEndpointAuthMethod represents an OAuth 2.0 token endpoint authentication method as defined in RFC 6749 §2.3 and advertised by authorization servers via the "token_endpoint_auth_methods_supported" metadata field (RFC 8414 §2). The client uses SelectAuthMethod to negotiate which method to use based on what the AS supports.

See: https://www.rfc-editor.org/rfc/rfc6749#section-2.3 See: https://www.rfc-editor.org/rfc/rfc8414#section-2 (discovery metadata)

const (
	// AuthMethodNone indicates a public client with no client secret.
	// Only the client_id is sent as a form body parameter. This is the
	// correct method for native/SPA clients using PKCE without a secret.
	AuthMethodNone TokenEndpointAuthMethod = "none"

	// AuthMethodClientSecretPost sends client_id and client_secret as
	// form body parameters in the token request. Less secure than Basic
	// because credentials appear in the request body (potentially logged
	// by proxies or WAFs), but required by some AS implementations.
	AuthMethodClientSecretPost TokenEndpointAuthMethod = "client_secret_post"

	// AuthMethodClientSecretBasic sends client credentials via the HTTP
	// Basic authentication scheme (RFC 7617) in the Authorization header:
	// "Authorization: Basic base64(client_id:client_secret)". This is the
	// RFC 6749 §2.3.1 default and preferred method because credentials
	// stay out of the request body.
	AuthMethodClientSecretBasic TokenEndpointAuthMethod = "client_secret_basic"
)
const AuthMethodPrivateKeyJWT TokenEndpointAuthMethod = "private_key_jwt"

AuthMethodPrivateKeyJWT names the RFC 7521 §4.2 / RFC 7523 §2.2 / OIDC Core §9 token-endpoint client authentication method where the client signs a JWT with its registered private key and sends it as `client_assertion`. Strongest of the standard methods — there is no shared secret to leak.

func SelectAuthMethod added in v0.0.65

func SelectAuthMethod(clientSecret string, asMethods []string) TokenEndpointAuthMethod

SelectAuthMethod chooses the appropriate token endpoint authentication method based on the client's credentials and the AS's advertised token_endpoint_auth_methods_supported metadata.

Decision logic:

  1. No client secret → "none" (public client, e.g., PKCE-only native apps)
  2. AS advertises methods → pick best supported match, preferring client_secret_basic over client_secret_post (credentials in header are less likely to be logged than credentials in body)
  3. AS doesn't advertise methods (nil/empty) → default to client_secret_basic per RFC 6749 §2.3.1
  4. AS advertises only unknown methods (e.g., private_key_jwt) → fall back to client_secret_basic as a safe default

This function is used by both LoginWithBrowser (auth code + PKCE flow) and ClientCredentialsToken (machine-to-machine flow) to negotiate how credentials are sent to the token endpoint.

See: https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1 See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.1 See: https://github.com/panyam/oneauth/issues/72

type TokenSource added in v0.0.68

type TokenSource interface {
	Token() (string, error)
}

TokenSource provides OAuth2 access tokens. This interface matches mcpkit/core.TokenSource by structural typing — no cross-module import needed.

type UpdateRegistrationRequest added in v0.0.81

type UpdateRegistrationRequest struct {
	RegistrationClientURI   string
	RegistrationAccessToken string
	// ClientID is the client's existing client_id. Required by RFC 7592 §2.2;
	// auto-filled into Metadata.ClientID by the SDK before sending if the
	// caller hasn't already set it.
	ClientID string
	// Metadata is the full RFC 7591/7592 client metadata that will replace
	// the existing registration. Treated as a full replacement (not PATCH).
	Metadata ClientRegistrationRequest
	// HTTPClient — see GetRegistrationRequest for rationale.
	HTTPClient *http.Client
}

UpdateRegistrationRequest is the input to UpdateRegistration.

type UpdateRegistrationResponse added in v0.0.81

type UpdateRegistrationResponse struct {
	Registration *ClientRegistrationResponse
}

UpdateRegistrationResponse wraps the post-update registration. Registration includes the rotated registration_access_token, which supersedes the one in the request. Callers MUST persist the new token before discarding the old one.

func UpdateRegistration added in v0.0.81

UpdateRegistration performs an RFC 7592 §2.2 full-replace update.

On success the AS rotates the registration_access_token; the new token surfaces on the response.

Maps server responses:

  • 200 OK → returns the parsed response (with the rotated token)
  • 401 → ErrRegistrationUnauthorized
  • 400 → returns a generic error including the AS's error_description
  • others → generic error including status + body

See: https://www.rfc-editor.org/rfc/rfc7592#section-2.2

Directories

Path Synopsis
stores
fs

Jump to

Keyboard shortcuts

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