Documentation
¶
Overview ¶
Package client provides client-side authentication utilities for oneauth. It includes credential storage, automatic token refresh, and HTTP client helpers.
Index ¶
- Constants
- func FollowRedirects(httpClient *http.Client) func(string) error
- type ASMetadata
- type AuthClient
- func (c *AuthClient) ClientCredentialsToken(clientID, clientSecret string, scopes []string) (*ServerCredential, error)
- func (c *AuthClient) GetCredential() (*ServerCredential, error)
- func (c *AuthClient) GetToken() (string, error)
- func (c *AuthClient) HTTPClient() *http.Client
- func (c *AuthClient) IsLoggedIn() bool
- func (c *AuthClient) Login(username, password, scope string) (*ServerCredential, error)
- func (c *AuthClient) LoginWithBrowser(cfg BrowserLoginConfig) (*ServerCredential, error)
- func (c *AuthClient) Logout() error
- func (c *AuthClient) ServerURL() string
- type AuthTransport
- type BrowserLoginConfig
- type ClientOption
- type CredentialStore
- type DiscoveryOption
- type OAuth2TokenRequest
- type OAuth2TokenResponse
- type ServerCredential
- type TokenEndpointAuthMethod
Constants ¶
const RefreshThreshold = 5 * time.Minute
RefreshThreshold is how long before expiry to proactively refresh
Variables ¶
This section is empty.
Functions ¶
func FollowRedirects ¶ added in v0.0.65
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.
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"`
}
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):
- https://auth.example.com/.well-known/oauth-authorization-server
- https://auth.example.com/.well-known/openid-configuration
For issuer "https://auth.example.com/tenant1" (with path):
- https://auth.example.com/.well-known/oauth-authorization-server/tenant1
- https://auth.example.com/tenant1/.well-known/openid-configuration
Returns the first successful response. Returns an error if all attempts fail.
type AuthClient ¶
type AuthClient struct {
// 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
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) 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:
- Generates PKCE verifier + challenge
- Generates a random state parameter for CSRF protection
- Starts a temporary loopback HTTP server to catch the redirect
- Opens the user's browser to the authorization URL
- Waits for the callback with the authorization code
- Validates the state parameter
- Exchanges the code for tokens using the code_verifier
- 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
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
}
BrowserLoginConfig configures the authorization code + PKCE flow for CLI and headless clients.
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.
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 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 DiscoveryOption ¶ added in v0.0.56
type DiscoveryOption func(*discoveryConfig)
DiscoveryOption configures the discovery request.
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 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"`
Error string `json:"error,omitempty"`
ErrorDesc string `json:"error_description,omitempty"`
}
OAuth2TokenResponse is the response from token endpoint
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"`
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" )
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:
- No client secret → "none" (public client, e.g., PKCE-only native apps)
- 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)
- AS doesn't advertise methods (nil/empty) → default to client_secret_basic per RFC 6749 §2.3.1
- 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