Documentation
¶
Overview ¶
Package auth is the CLI's authentication core. It implements an OAuth 2.1 client (browser-based loopback Authorization Code + PKCE with rotating refresh tokens) and unifies it with the legacy long-lived API key behind a single Authorizer abstraction.
Authentication is OAuth-first, API-key-second. The resolution order (see Resolve) is:
- an explicitly-provided API key (--api-key flag or TRUESTAMP_API_KEY env) — wins outright so CI/headless callers are deterministic;
- a stored OAuth session (access token, auto-refreshed via the rotating refresh token);
- an API key from the config file;
- otherwise unauthenticated.
Both an OAuth access token and an API key are presented to the server as `Authorization: Bearer <value>` on the JSON API; the server accepts either. The console WebSocket differs: OAuth uses the Bearer request header on the upgrade, while an API key uses the `?api_key=` query param — callers branch on Authorizer.Mode for that.
Index ¶
- Constants
- Variables
- func AuthorizeRequest(ctx context.Context, req *http.Request) error
- func IsInvalidGrant(err error) bool
- func Logout(ctx context.Context, store Store) (revoked bool, err error)
- func NewRetryTransport(base http.RoundTripper) http.RoundTripper
- func SetDefault(a Authorizer)
- type Authorizer
- type Credentials
- type Discovery
- type LoginOptions
- type Mode
- type Session
- type Store
Constants ¶
const ClientID = "019ef661-6737-71ec-abd0-ac8f4684ce45"
ClientID is the fixed, public OAuth client_id for the Truestamp CLI. It is a public client (PKCE is the per-flow secret), so embedding it in the binary is standard and safe — the same convention gh/gcloud/stripe use. The server seeds this exact id via an idempotent boot-upsert across dev/staging/prod, so a single constant is correct for every environment; only the issuer/endpoints vary, and those come from discovery.
Variables ¶
var ErrNoCredentials = errors.New("not authenticated — run `truestamp auth login` (or set TRUESTAMP_API_KEY)")
ErrNoCredentials is returned by a no-credentials Authorizer when a bearer token is requested (e.g. by the WebSocket dialer, which cannot proceed unauthenticated).
var ErrNoSession = errors.New("no oauth session stored")
ErrNoSession means no OAuth session is stored for the origin.
var ErrSessionExpired = errors.New("OAuth session expired or revoked — run `truestamp auth login`")
ErrSessionExpired is returned in OAuth mode when the refresh token is expired, revoked, or reused (the server's `invalid_grant`). The session is permanently dead; the user must re-authenticate.
var Scopes = []string{"api:read", "api:write", "console:read", "console:write"}
Scopes is the set requested at login. It deliberately excludes the mcp:* scopes (the CLI does not call /mcp); requesting an mcp scope is rejected at /oauth/authorize. api:* covers JSON:API + GraphQL, console:* covers the WebSocket console.
Functions ¶
func AuthorizeRequest ¶
AuthorizeRequest stamps req using the process-wide Default Authorizer. This is the minimal-churn entry point for the existing HTTP call sites.
func IsInvalidGrant ¶
IsInvalidGrant reports whether err is an OAuth `invalid_grant` token error — the signal that a refresh token is expired, reused, or revoked and the session is permanently dead (the caller should stop retrying and prompt re-login).
func Logout ¶
Logout best-effort revokes the stored refresh token (RFC 7009) and clears the local session. Revocation failures are non-fatal — the local session is cleared regardless, and access tokens are short-lived stateless JWTs.
func NewRetryTransport ¶
func NewRetryTransport(base http.RoundTripper) http.RoundTripper
NewRetryTransport wraps base (or http.DefaultTransport) with the reactive 401 → refresh → retry-once behavior. Install it on the shared HTTP client.
func SetDefault ¶
func SetDefault(a Authorizer)
SetDefault installs the process-wide Authorizer. Pass nil to reset to the no-credentials authorizer.
Types ¶
type Authorizer ¶
type Authorizer interface {
// Authorize adds the Authorization header to req. In OAuth mode it
// transparently refreshes an expired access token first (returning a
// non-nil error if the session is dead). In no-credentials mode it is
// a no-op returning nil so genuinely-public requests still go out.
Authorize(ctx context.Context, req *http.Request) error
// BearerToken returns the raw credential string — a fresh OAuth access
// token (refreshed if needed) or the API key — for carriers that
// cannot take an *http.Request (the WebSocket dialer). Returns
// [ErrNoCredentials] in no-credentials mode.
BearerToken(ctx context.Context) (string, error)
// ForceRefresh proactively discards any cached credential and obtains a
// fresh one. In OAuth mode it runs the refresh grant unconditionally
// (used reactively when the server rejects a token the client still
// believes is valid — a 401 on the HTTP API, or a token_expired push on
// the WebSocket — to break the otherwise-possible "reuse the stale
// token" loop). It is a no-op for the API-key and no-credentials modes.
// Returns [ErrSessionExpired] when the refresh token is dead.
ForceRefresh(ctx context.Context) error
// AccessTokenExpiry reports when the current OAuth access token expires,
// so a long-lived carrier (the console WebSocket) can proactively
// refresh in-band before the server force-disconnects. Returns the zero
// time for the API-key and no-credentials modes (no expiry).
AccessTokenExpiry() time.Time
// Mode reports the active credential type.
Mode() Mode
}
Authorizer stamps outbound requests with the active credential and reports which credential is in use. Obtain one via Resolve. All methods are safe for concurrent use.
func APIKeyAuthorizer ¶
func APIKeyAuthorizer(key string) Authorizer
APIKeyAuthorizer returns an Authorizer that presents key as a Bearer token. Exported for cmd wiring and for tests that exercise the API-key path directly.
func Resolve ¶
func Resolve(creds Credentials, store Store) Authorizer
Resolve selects the active credential per the documented precedence: explicit API key → OAuth session → config-file API key → none.
type Credentials ¶
type Credentials struct {
// APIKey is the merged api_key value (config file, env, or flag).
APIKey string
// APIKeyExplicit is true when the key came from an intentional
// override — the --api-key flag or TRUESTAMP_API_KEY env — as opposed
// to the config file. An explicit key wins over an OAuth session so
// CI/headless behavior is deterministic.
APIKeyExplicit bool
}
Credentials carries the resolved API-key state used by Resolve.
type Discovery ¶
type Discovery struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
RegistrationEndpoint string `json:"registration_endpoint"`
GrantTypesSupported []string `json:"grant_types_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
ScopesSupported []string `json:"scopes_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
}
Discovery is the subset of RFC 8414 authorization-server metadata the CLI consumes. Endpoints are read from here (never hardcoded) so the same binary works against dev/staging/prod, which differ only by origin.
func Fetch ¶
Fetch retrieves and parses the authorization-server metadata for the given base origin (scheme + host, e.g. "http://localhost:4000"). It reuses the shared HTTP client (timeout, User-Agent, response-size cap).
func (*Discovery) Validate ¶
Validate checks that the discovery document is usable for the loopback PKCE flow against the expected origin: it must carry the authorization and token endpoints, advertise S256 PKCE, and its issuer must match the configured origin (a guard against a misconfigured base_url silently authenticating against the wrong server).
type LoginOptions ¶
type LoginOptions struct {
// Open opens a URL in the user's browser. Defaults to browser.OpenURL.
Open func(string) error
// Out receives the human-facing "open this URL" guidance. Defaults to
// io.Discard.
Out io.Writer
}
LoginOptions tunes the interactive login flow. The zero value is valid: it opens the system browser and writes prompts to os.Stderr-equivalent nil (suppressed). cmd populates Out and may override Open for tests.
type Session ¶
type Session struct {
Issuer string `json:"issuer"`
AuthURL string `json:"auth_url"`
TokenURL string `json:"token_url"`
RevocationURL string `json:"revocation_url,omitempty"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
Expiry time.Time `json:"expiry"`
}
Session is the persisted OAuth state for one server origin. The endpoint URLs are captured at login so token refresh and revocation never need a fresh discovery round-trip during normal operation.
func Login ¶
func Login(ctx context.Context, baseOrigin string, store Store, opts LoginOptions) (*Session, error)
Login runs the browser-based loopback Authorization Code + PKCE flow against baseOrigin, persists the resulting session via store, and returns it. ctx bounds the whole flow (caller should pass a generous deadline — the user has to sign in and consent in a browser).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists [Session]s for a single server origin. It prefers the OS keychain (macOS Keychain, libsecret/SecretService, Windows credential manager) and transparently falls back to a 0600 JSON file under the user cache dir when no keychain is available (e.g. headless Linux). Reads consult both backends so a session written under one is still found if availability changes between runs.
func (Store) Clear ¶
Clear removes the session from both backends. Missing entries are not an error (logout is idempotent).
func (Store) Load ¶
Load returns the stored session for the origin, or ErrNoSession.