auth

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 19 Imported by: 0

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:

  1. an explicitly-provided API key (--api-key flag or TRUESTAMP_API_KEY env) — wins outright so CI/headless callers are deterministic;
  2. a stored OAuth session (access token, auto-refreshed via the rotating refresh token);
  3. an API key from the config file;
  4. 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

View Source
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

View Source
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).

View Source
var ErrNoSession = errors.New("no oauth session stored")

ErrNoSession means no OAuth session is stored for the origin.

View Source
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.

View Source
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

func AuthorizeRequest(ctx context.Context, req *http.Request) error

AuthorizeRequest stamps req using the process-wide Default Authorizer. This is the minimal-churn entry point for the existing HTTP call sites.

func IsInvalidGrant

func IsInvalidGrant(err error) bool

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

func Logout(ctx context.Context, store Store) (revoked bool, err error)

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 Default

func Default() Authorizer

Default returns the process-wide Authorizer (never nil).

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

func Fetch(ctx context.Context, baseOrigin string) (*Discovery, error)

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

func (d *Discovery) Validate(baseOrigin string) error

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 Mode

type Mode int

Mode identifies which credential an Authorizer carries.

const (
	// ModeNone means no credential is configured.
	ModeNone Mode = iota
	// ModeAPIKey means a long-lived API key is in use.
	ModeAPIKey
	// ModeOAuth means a short-lived, auto-refreshing OAuth access token
	// is in use.
	ModeOAuth
)

func (Mode) String

func (m Mode) String() string

String renders the mode for status output.

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 NewStore

func NewStore(baseURL string) Store

NewStore returns a Store keyed by the canonical origin of baseURL.

func (Store) Clear

func (s Store) Clear() error

Clear removes the session from both backends. Missing entries are not an error (logout is idempotent).

func (Store) Load

func (s Store) Load() (Session, error)

Load returns the stored session for the origin, or ErrNoSession.

func (Store) Location

func (s Store) Location() string

Location reports where a session for this origin is persisted, for display after login. It probes the keychain first, then the file fallback, so the message reflects the backend actually used.

func (Store) Save

func (s Store) Save(sess Session) error

Save persists the session, preferring the keychain and falling back to the 0600 file when the keychain write fails (unsupported/unavailable).

Jump to

Keyboard shortcuts

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