clientauth

package
v0.0.34 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package clientauth provides durable, target-bound public-client OIDC credentials.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrDiscovery indicates that OIDC discovery was rejected.
	ErrDiscovery = errors.New("clientauth: OIDC discovery rejected")
	// ErrAuthorization indicates that authorization failed.
	ErrAuthorization = errors.New("clientauth: authorization failed")
	// ErrTokenExchange indicates that the token exchange failed.
	ErrTokenExchange = errors.New("clientauth: token exchange failed")
	// ErrLoginRequired indicates that interactive login is required.
	ErrLoginRequired = errors.New("clientauth: login required")
	// ErrCredentialCleanup indicates that a rejected credential could not be removed safely.
	ErrCredentialCleanup = errors.New("clientauth: credential cleanup failed")
)
View Source
var (
	// ErrInvalidIdentity indicates invalid target identity data.
	ErrInvalidIdentity = errors.New("clientauth: invalid target identity")
	// ErrKeyUnavailable indicates that the credential encryption key is unavailable.
	ErrKeyUnavailable = errors.New("clientauth: encryption key unavailable")
	// ErrCorrupt indicates malformed or invalid stored credentials.
	ErrCorrupt = errors.New("clientauth: corrupt credential")
)
View Source
var ErrIncompleteEnrollment = errors.New("clientauth: enrollment incomplete")

ErrIncompleteEnrollment means enrollment reached an ambiguous or partially reconciled local state. It never implies crash atomicity; a caller may safely inspect the registry and retry.

View Source
var ErrIncompleteLogout = errors.New("clientauth: logout incomplete")

ErrIncompleteLogout indicates that local state could not be fully reconciled. Provider revocation failures do not produce this error: local logout remains successful when the issuer is unavailable.

View Source
var ErrTargetChanged = errors.New("clientauth: target changed during sign-in")

ErrTargetChanged means the target's registry or credential state at commit time no longer matches the ExpectedTarget/ExpectedCredential snapshot the caller supplied. Enroll performs no write in this case; the caller should report a distinct recovery reason rather than either silently succeeding (which could resurrect a logged-out target or clobber a newer enrollment) or crashing.

Functions

func Enroll

func Enroll(ctx context.Context, conn Connection, token Token, cfg EnrollmentConfig) error

Enroll stores a login token and makes conn the sole registry entry for its target. The caller must complete the interactive login before calling Enroll: target serialization begins only once a token is ready.

func IsNotEnrolled

func IsNotEnrolled(err error) bool

IsNotEnrolled reports whether err means nothing is stored for the target yet, as opposed to a registry or credential store that cannot be read. Callers must separate the two: the first is answered by running a login, the second is a local fault, and collapsing them turns an unenrolled target into what looks like a broken installation.

func IssuerHTTPClient added in v0.0.23

func IssuerHTTPClient(ctx context.Context, policy IssuerAddressPolicy, issuer string, trustedCAPEM []byte) (*http.Client, error)

IssuerHTTPClient builds the HTTP client used for one saved connection's OIDC issuer endpoints -- discovery, token exchange, JWKS, refresh, and revocation. It is the SINGLE mapping from a persisted IssuerAddressPolicy to a transport, shared by login/refresh (oidcClient) and logout, so the two cannot drift.

The two policies are genuinely different problems, not two settings of one (ADR 0279):

  • private: the operator supplies an internal CA that legitimately signs MANY internal services, so TLS alone cannot say WHICH one answered. The scoped transport adds the missing control -- an approved-authority allowlist with per-authority root pools and pinned DNS answers. The CA is mandatory.

  • public: verification is ordinary WebPKI. That is already the control, so the scoped machinery is omitted; see newPublicIssuerClient.

func OpenCredentialStore added in v0.0.29

func OpenCredentialStore(ctx context.Context, root string, backend CredentialBackend) (credentialstore.Store, error)

OpenCredentialStore opens the selected creating store, never choosing a backend.

func OpenExistingStore

func OpenExistingStore(ctx context.Context, root string, keys ExistingKeyProvider) (*credentialstore.EncryptedFileStore, error)

OpenExistingStore opens the encrypted namespace without generating keyring state.

func OpenStore

OpenStore opens this adapter's isolated encrypted credential namespace.

Types

type Connection

type Connection struct {
	Identity Identity `json:"identity"`
	// ResourceURL is the optional canonical HTTPS protected-resource identity.
	// It deliberately remains outside Identity so existing credential record keys
	// stay stable across this additive registry metadata change.
	ResourceURL         string              `json:"resource_url,omitempty"`
	IssuerCAFile        string              `json:"issuer_ca_file,omitempty"`
	IssuerAddressPolicy IssuerAddressPolicy `json:"issuer_address_policy,omitempty"`
}

Connection is non-secret saved connection metadata. IssuerCAFile is used only for OIDC discovery, JWKS, refresh, and revocation; it is not server transport trust. UnmarshalJSON accepts the legacy tls_ca_file name for registry compatibility.

func (*Connection) UnmarshalJSON

func (c *Connection) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the former tls_ca_file field as issuer trust. When both are present, the explicit issuer_ca_file value wins, including an explicit empty value.

type CredentialBackend added in v0.0.29

type CredentialBackend string

CredentialBackend is the durable, closed backend vocabulary.

const (
	CredentialBackendKeyring CredentialBackend = "keyring"
	CredentialBackendFile    CredentialBackend = "file"
)

Supported durable backends.

func OpenExistingCredentialStore added in v0.0.29

func OpenExistingCredentialStore(ctx context.Context, root string) (credentialstore.Store, CredentialBackend, error)

OpenExistingCredentialStore uses the existing pin, or pins valid legacy evidence.

type CredentialRecord

type CredentialRecord struct {
	Token   Token
	Version credentialstore.Version
}

CredentialRecord carries an opaque CAS version.

type CredentialStoreMode added in v0.0.29

type CredentialStoreMode string

CredentialStoreMode is the login-only backend selector.

const (
	CredentialStoreAuto    CredentialStoreMode = "auto"
	CredentialStoreKeyring CredentialStoreMode = "keyring"
	CredentialStoreFile    CredentialStoreMode = "file"
)

Supported login-only selectors.

type CredentialStoreSelection added in v0.0.29

type CredentialStoreSelection struct {
	Backend     CredentialBackend
	NewlyPinned bool
}

CredentialStoreSelection reports the root's authoritative backend.

func ResolveCredentialStore added in v0.0.29

func ResolveCredentialStore(ctx context.Context, root string, requested CredentialStoreMode) (CredentialStoreSelection, error)

ResolveCredentialStore selects and pins a backend before interactive OAuth.

type Credentials

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

Credentials is a target-bound, CAS-safe credential repository.

func NewCredentials

func NewCredentials(store credentialstore.Store) (*Credentials, error)

NewCredentials creates a credential repository backed by store.

func (*Credentials) Delete

func (c *Credentials) Delete(ctx context.Context, identity Identity, expected credentialstore.Version) error

Delete removes credentials for identity using expected CAS version.

func (*Credentials) Load

func (c *Credentials) Load(ctx context.Context, identity Identity) (CredentialRecord, error)

Load retrieves and validates credentials for identity.

func (*Credentials) Save

func (c *Credentials) Save(ctx context.Context, identity Identity, token Token, expected *credentialstore.Version) (CredentialRecord, error)

Save stores credentials for identity using optional CAS version expected.

func (*Credentials) Upsert

func (c *Credentials) Upsert(ctx context.Context, identity Identity, token Token) (CredentialRecord, error)

Upsert stores token for identity, replacing any credential already held for it. Enrolment is repeatable BY DESIGN: a login must be able to replace an expired, revoked, or rotated credential, so it cannot use Save's create-only precondition. Save's CAS is for the refresh path, where losing the race means another process rotated the token first and this one's write is stale.

Upsert deliberately remains ordinary load-then-CAS convenience. Target-locked reauthentication of a corrupt current record goes through Enroll, which can prove registry reachability and use the store's conditional recovery seam.

type EnrollmentConfig

type EnrollmentConfig struct {
	Registry    *Registry
	Credentials *Credentials
	// ExpectedTarget, when non-nil, is the target's connection snapshot taken
	// BEFORE an interactive step (e.g. a browser OAuth exchange) that can run
	// arbitrarily long. Enroll then requires the target's state at commit
	// time to match this snapshot exactly, returning ErrTargetChanged
	// otherwise -- closing the gap between a reauthentication preflight and
	// its eventual write-back, across which another process could log the
	// target out. A nil ExpectedTarget (the default, used by a fresh
	// enrollment) enrolls unconditionally.
	ExpectedTarget *[]Connection
	// ExpectedCredential, when non-nil, is the SAME identity's own credential
	// snapshot taken at the same preflight time. ExpectedTarget alone cannot
	// detect a newer credential enrolled for the identical identity (the
	// registry's connection metadata is unchanged; only the credential
	// store's version moved), so this closes that half of the same race:
	// Enroll rejects with ErrTargetChanged rather than overwrite a credential
	// newer than the one the caller preflighted against. A credential that
	// was corrupt at preflight time (ExpectedCredentialState.Corrupt) still
	// constrains the repair path -- see checkExpectedCredential -- rather
	// than leaving it entirely unconstrained.
	ExpectedCredential *ExpectedCredentialState
}

EnrollmentConfig supplies the two durable halves of an enrollment.

type ExistingKeyProvider

type ExistingKeyProvider interface {
	ExistingStoreKey(context.Context) ([]byte, error)
}

ExistingKeyProvider supplies an already-created store key without creating one. Destructive and read-only operations use this so an idempotent logout cannot create new keyring state.

type ExpectedCredentialState

type ExpectedCredentialState struct {
	Found bool
	// Corrupt records that the preflight read hit ErrCorrupt for this
	// identity. Version is meaningless when Corrupt is true -- see
	// checkExpectedCredential.
	Corrupt bool
	Version credentialstore.Version
}

ExpectedCredentialState is Enroll's optional precondition on the enrolled identity's OWN credential state (not the registry's connection metadata -- see EnrollmentConfig.ExpectedTarget for that), captured before an interactive step that can run arbitrarily long.

type Identity

type Identity struct {
	Target, Issuer, ClientID, Audience, RedirectURI string
	Scopes                                          []string
}

Identity binds a credential to one canonical remote target and complete public-client configuration.

func (Identity) Canonical

func (i Identity) Canonical() (Identity, error)

Canonical validates and normalizes the identity.

func (Identity) Equal

func (i Identity) Equal(other Identity) bool

Equal reports whether two identities denote the same enrolment. Identity holds a slice, so it is not comparable with ==; callers outside this package need this.

type IncompleteLogoutError

type IncompleteLogoutError struct{}

IncompleteLogoutError carries only the secret-free outcome for a failed local reconciliation.

func (*IncompleteLogoutError) Error

func (*IncompleteLogoutError) Error() string

func (*IncompleteLogoutError) Unwrap

func (*IncompleteLogoutError) Unwrap() error

type IssuerAddressPolicy added in v0.0.23

type IssuerAddressPolicy string

IssuerAddressPolicy controls which network addresses the issuer transport may dial.

const (
	IssuerAddressPolicyPrivate IssuerAddressPolicy = "private"
	IssuerAddressPolicyPublic  IssuerAddressPolicy = "public"
)

The closed issuer address-policy vocabulary. A saved row carrying anything else is quarantined rather than defaulted (see Connection.UnmarshalJSON).

type KeyProvider

type KeyProvider interface {
	StoreKey(context.Context) ([]byte, error)
}

KeyProvider supplies exactly 32 bytes of encryption material. It must never persist keys beside credentials.

type KeyringProvider

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

KeyringProvider stores one root-scoped encryption key in the OS credential manager.

func NewExistingKeyringProvider

func NewExistingKeyringProvider(root string) (*KeyringProvider, error)

NewExistingKeyringProvider binds to keyring state that is already present. It never creates the store root or its lock, and never migrates legacy state.

func NewKeyringProvider

func NewKeyringProvider(root string) (*KeyringProvider, error)

NewKeyringProvider binds an OS-keyring provider to one absolute, clean store root.

func (*KeyringProvider) ExistingStoreKey

func (p *KeyringProvider) ExistingStoreKey(ctx context.Context) ([]byte, error)

ExistingStoreKey returns or migrates an existing key without generating one.

func (*KeyringProvider) StoreKey

func (p *KeyringProvider) StoreKey(ctx context.Context) ([]byte, error)

StoreKey returns the key used to protect the credential store, creating it if absent.

type LoginConfig

type LoginConfig struct {
	Identity   Identity
	Presenter  Presenter
	HTTPClient *http.Client
	// IssuerAddressPolicy selects the managed issuer transport. It is REQUIRED
	// unless the caller supplies its own HTTPClient (tests and fixtures); the two
	// are mutually exclusive, and there is no unmanaged default. See oidcClient.
	IssuerAddressPolicy IssuerAddressPolicy
	TrustedCAPEM        []byte
	// Registry supplies target-scoped transaction locking to RefreshSource. Login
	// itself does not use it, so browser interaction remains outside the lock.
	Registry *Registry
}

LoginConfig defines a public OAuth client. ClientSecret is intentionally absent.

type LoginRequiredCause

type LoginRequiredCause string

LoginRequiredCause explains why a credential can no longer be used. The set is deliberately closed: server-side authentication rejection is a transport concern.

const (
	// NotEnrolled means no credential is stored for the target.
	NotEnrolled LoginRequiredCause = "not_enrolled"
	// SessionExpired means the credential cannot be renewed.
	SessionExpired LoginRequiredCause = "session_expired"
	// CredentialUnusable means stored credential data is corrupt or malformed.
	// #nosec G101 -- diagnostic label, not a credential.
	CredentialUnusable LoginRequiredCause = "credential_unusable"
)

type LoginRequiredError

type LoginRequiredError struct{ Cause LoginRequiredCause }

LoginRequiredError retains ErrLoginRequired for compatibility while exposing a safe, local diagnosis. It never contains provider responses or credentials.

func (*LoginRequiredError) Error

func (e *LoginRequiredError) Error() string

func (*LoginRequiredError) Unwrap

func (*LoginRequiredError) Unwrap() error

type LogoutConfig

type LogoutConfig struct {
	Registry    *Registry
	Credentials *Credentials
	HTTPClient  func(context.Context, []Connection) (*http.Client, error)
	// HTTPClientForConnection builds an isolated managed client for one retained
	// connection. It prevents roots or address policy from crossing authorities.
	HTTPClientForConnection func(context.Context, Connection) (*http.Client, bool, error)
}

LogoutConfig supplies local state and an optional issuer-scoped HTTP client. A nil Credentials repository retains registry metadata rather than making a credential unreachable. HTTPClient failures affect revocation only. Both client builders are called AT MOST ONCE per Logout call, with every retained connection needing revocation, so a single client (its scopedhttps dial-approval policy spans every retained issuer) is reused across the whole operation instead of rebuilt per credential.

type LogoutIssue

type LogoutIssue struct {
	Identity Identity
	Stage    string
}

LogoutIssue is a secret-free description of local state that logout could not remove.

type LogoutResult

type LogoutResult struct {
	Target               string
	Entries              int
	CredentialsDeleted   int
	CredentialsMissing   int
	RegistryDeleted      bool
	RevocationsAttempted int
	RevocationsFailed    int
	// RevocationError is the first cause of a revocation failure (a DNS,
	// discovery, or HTTP error), secret-free. Empty unless RevocationsFailed > 0.
	RevocationError string
	Issues          []LogoutIssue
}

LogoutResult reports exactly which local halves were removed. It never contains tokens.

func Logout

func Logout(ctx context.Context, target string, cfg LogoutConfig) (LogoutResult, error)

Logout removes credentials before their target metadata. CAS conflicts and unreadable credentials retain the registry entry so another process's token rotation never becomes an unreachable orphan. Provider revocation is bounded best effort and never blocks local deletion.

type Presenter

type Presenter interface {
	Present(context.Context, string) (oauthlogin.Result, error)
}

Presenter is implemented by the host-owned oauthlogin callback runtime. This package neither opens a browser nor listens for callbacks.

type PresenterFunc

type PresenterFunc func(context.Context, string) (oauthlogin.Result, error)

PresenterFunc adapts a function to the Presenter interface.

func (PresenterFunc) Present

func (f PresenterFunc) Present(ctx context.Context, u string) (oauthlogin.Result, error)

Present invokes f with the authorization URL.

type RefreshSource

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

RefreshSource validates access tokens and refreshes them only when necessary. It is safe for gRPC's concurrent per-RPC credential calls.

func NewRefreshSource

func NewRefreshSource(ctx context.Context, creds *Credentials, cfg LoginConfig) (*RefreshSource, error)

NewRefreshSource constructs a target-bound source. Call Close when the dial is done.

func (*RefreshSource) Close

func (s *RefreshSource) Close() error

Close releases resources held by the refresh source and waits for proactive refresh to stop before closing owned clients and the validator.

func (*RefreshSource) Token

func (s *RefreshSource) Token(ctx context.Context) (string, error)

Token returns a validated access token and persists a rotated token using CAS.

type Registry

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

Registry is an owner-only, atomic, non-secret connection metadata registry.

func OpenExistingRegistry

func OpenExistingRegistry(root string) (*Registry, error)

OpenExistingRegistry opens registry metadata without creating the root or registry file. The returned Registry's lock is real but inert (flock.New performs no I/O until Lock/TryLock is called), so a subsequent write through Enroll's existing-target transaction can still serialize correctly; a write against a root that was never created still fails cleanly (os.CreateTemp on a missing directory, or the lock file's own missing parent) rather than silently materializing new state.

func OpenRegistry

func OpenRegistry(root string) (*Registry, error)

OpenRegistry opens the connection metadata registry rooted at root.

func (*Registry) DeleteTarget

func (r *Registry) DeleteTarget(target string, expected []Connection) (int, error)

DeleteTarget removes every registry entry for target when those entries still equal expected. The expected snapshot is the registry-side CAS: a concurrent re-enrolment is retained rather than being removed by an older logout.

func (*Registry) Find added in v0.0.26

func (r *Registry) Find(alias string) (Connection, error)

Find returns the one saved connection addressed by an exact canonical resource URL or target. Resource and target aliases are intentionally resolved through the same ambiguity check; a legacy row without ResourceURL is target-only. An alias outside every recognized grammar (including a legacy `scheme://host:port` gRPC target such as `unix://...`, which predates canonicalTarget's own stricter grammar) cannot name a saved row and reports credentialstore.ErrNotFound rather than ErrInvalidIdentity, mirroring FindTarget's leniency: both real callers (cmd/mecatui's connect and resolveTransport) treat "not enrolled" as the ordinary, idempotent miss and anything else as a hard local-storage failure.

func (*Registry) FindTarget

func (r *Registry) FindTarget(target string) (Connection, error)

FindTarget returns the saved connection for target. Registry read failures take precedence; a target outside the enrollment grammar cannot name a saved row and returns credentialstore.ErrNotFound after a clean read.

func (*Registry) List

func (r *Registry) List() ([]Connection, error)

List returns all saved connections.

func (*Registry) Upsert

func (r *Registry) Upsert(conn Connection) ([]Identity, error)

Upsert stores conn as the single enrolment for its target and returns the identities it superseded. Credentials are keyed by identity, not target, so a caller holding the credential store must discard those: otherwise a superseded refresh token stays on disk indefinitely, unreachable and unrevoked.

type Token

type Token struct {
	AccessToken, RefreshToken, TokenType string
	Expiry                               string
}

Token is the secret OAuth material, deliberately absent from Registry records.

func Login

func Login(ctx context.Context, cfg LoginConfig) (Token, error)

Login runs one Authorization Code + PKCE S256 exchange and validates the initial access token.

Jump to

Keyboard shortcuts

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