Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 42 Imported by: 0

Documentation

Overview

Package auth provides OAuth 2.1 authentication for Basecamp.

Index

Constants

View Source
const (
	// RevokeSkippedLaunchpad: the legacy provider has no revocation endpoint.
	// Credentials from the removed "bc3" development flow land here too —
	// the server that minted them is gone.
	RevokeSkippedLaunchpad = "launchpad"
	// RevokeSkippedImported: an imported personal access token is not the
	// CLI's to revoke — the same token lives in the operator's secret store
	// and stays valid until revoked in Basecamp.
	RevokeSkippedImported = "imported_token"
	// RevokeSkippedAgent: an agent self-token is minted on demand from a
	// client id and secret, so revoking one accomplishes nothing — the
	// same client mints another on the next command. What ends this
	// credential's access is disconnecting its connection in Basecamp,
	// which kills that connection's secret and every token minted from it.
	// Once an agent can hold several connections, neither older remedy is
	// right: rotating the client secret (Basecamp has no control for it)
	// rewrites only the first connection's, and disconnecting the whole
	// agent ends every other computer's connection too.
	RevokeSkippedAgent = "agent"
)

Reasons a credential is removed locally without a revocation attempt.

View Source
const (
	// RemainingRefresh: the refresh token was not revoked, so the whole
	// family — its access token included — stays valid until it is.
	RemainingRefresh = "refresh_token"
	// RemainingAccess: only the access token remains (the refresh token was
	// revoked, or there never was one); it expires on its own.
	RemainingAccess = "access_token"
)

What a failed revocation left usable.

View Source
const AgentResourceURNPrefix = "urn:bc:agent:"

AgentResourceURNPrefix is the RFC 8707 resource indicator Basecamp binds an agent self-token to. The trailing segment is the agent's id.

View Source
const ClientEnvHint = "Set both BASECAMP_OAUTH_CLIENT_ID and BASECAMP_OAUTH_CLIENT_SECRET, or unset both to use the built-in client"

ClientEnvHint is the remedy for a half-set BASECAMP_OAUTH_CLIENT_ID and BASECAMP_OAUTH_CLIENT_SECRET pair. Logging in reads the same pair and fails the same way, so the login is no remedy; the environment is.

View Source
const CredentialSourceToken = "token"

CredentialSourceToken marks a credential imported from a personal access token rather than obtained through an OAuth flow.

View Source
const MaxServerWait = maxAgentConnectLifetime

MaxServerWait is the longest any wait a server names is waited, whatever the status or the token path: the bound a rate-limit hold is written under, so a held rate limit and every other named wait agree. A connector that waited a server's word for years would be a hang, not a wait.

View Source
const RefreshWindow = 5 * time.Minute

RefreshWindow is how long before its expiry a stored access token stops being served as-is: AccessToken refreshes inside this window, and a credential with nothing to refresh with (an imported token) is refused there instead. Exported so an import can decline a token that would be unusable from its first command.

Variables

View Source
var ErrAgentCredentialRefused = errors.New("the agent's credential was refused")

ErrAgentCredentialRefused is the cause of a mint the token endpoint refused for the credentials themselves: this connection may have been disconnected in Basecamp or replaced by a newer approval.

View Source
var ErrInvalidCredentials = errors.New("invalid credentials")

ErrInvalidCredentials marks a stored blob that is present but not a credential any command can read — as opposed to nothing stored at all.

View Source
var ErrNoCredential = errors.New("no stored credential")

ErrNoCredential marks a key with nothing stored under it — as opposed to a store that could not be read, which is not "not logged in".

Functions

func NamedWait added in v0.12.0

func NamedWait(err error) int

NamedWait is the wait, in seconds, the server itself named for err: more than RetryAfter when the wait was capped, the same otherwise.

func ProfileCredentialKey added in v0.12.0

func ProfileCredentialKey(profile string) string

ProfileCredentialKey is the credential key a named profile's credential is stored under, for a caller that must name one profile's credential without a manager bound to it.

func RetryAfter added in v0.12.0

func RetryAfter(err error) int

RetryAfter is the wait, in seconds, a rate-limited answer named, or zero when it named none.

func ValidateSession added in v0.18.0

func ValidateSession(id, label string) error

ValidateSession checks a session id and label against the rules the token endpoint holds them to, so a malformed one is a usage error here rather than a refused mint. The label is optional; the id is not.

Types

type AgentConnectIntake added in v0.14.0

type AgentConnectIntake struct {
	VerificationURI string
	UserCode        string
	ExpiresAt       time.Time
}

AgentConnectIntake is what a person needs to approve a connection: the link to open, the code to check against the page, and when the code dies. Both are the copies the terminal shows, stripped of control sequences: whoever reads them shows them to a person, and a JSON encoder passes UTF-8-encoded C1 controls through raw. The browser is still sent to the validated URL itself.

type AgentConnectOptions added in v0.12.0

type AgentConnectOptions struct {
	// DeviceName is where this connector runs, self-asserted and shown to
	// the operator on the approval page. Required, as the intake requires
	// it: an unnamed connection is one nobody can recognize later.
	DeviceName string

	// SoftwareName is the product this connector runs inside, shown as
	// "Connected to". Optional and self-asserted.
	SoftwareName string

	// Scope is the access to ask for ("read" or "full"); empty asks for
	// full, which the approval page offers to downgrade. What the operator
	// approves is what gets stored.
	Scope string

	// NoBrowser prints the link instead of opening it; Local forces a
	// launch the host heuristics would have skipped. Both mean what they
	// mean on a login, and are honored by the same code.
	NoBrowser bool
	Local     bool

	// BrowserLauncher opens the verification URL. Nil uses the system
	// browser, unless the flow is not launching one at all.
	BrowserLauncher func(url string) error

	// Logger receives the operator's half of the ceremony. Nil suppresses
	// it — which, since the ceremony is the operator reading a link and a
	// code, is only ever right for a test.
	Logger func(msg string)

	// Progress, when it is a terminal, carries the live wait line while
	// the poll runs, exactly as it does for a device login.
	Progress io.Writer

	// OnIntake, when set, is told what the operator needs to approve the
	// connection as soon as the intake has answered, before the browser is
	// opened and the wait begins: a caller reading the ceremony as data
	// learns the link and the code without parsing the operator's half.
	// It is never given the device code, which is the poll's bearer and
	// stays inside this flow. A non-nil error ends the ceremony before
	// the browser is opened or anything waits: the link has not reached
	// whoever was to show it, and nothing is stored.
	OnIntake func(AgentConnectIntake) error

	// BeforeStore, when set, runs after the poll has handed over the
	// credential and the mint has proved it, and before anything is
	// written. A non-nil error aborts the connection and stores nothing.
	// It is where the caller commits the profile entry the credential
	// belongs to — which is why it receives the connection: the account
	// is the server's to name, not the caller's.
	BeforeStore func(conn *AgentConnection) error
	// contains filtered or unexported fields
}

AgentConnectOptions configures the connection ceremony.

type AgentConnectResult added in v0.12.0

type AgentConnectResult struct {
	LoginResult
	AccountID string
	ClientID  string
}

AgentConnectResult is a completed connection: the credential it stored, and the two facts the caller needs that a login result does not carry — which account the operator connected, and which client is now this profile's.

type AgentConnection added in v0.12.0

type AgentConnection struct {
	ClientID     string
	ClientSecret string
	AccountID    string
	Scope        string
}

AgentConnection is what the poll hands back once the operator has approved: the agent's own confidential client, the account it belongs to, and the scope the operator approved — which is what the client was provisioned with, not necessarily what was asked for.

type ClientCredentials

type ClientCredentials struct {
	ClientID     string `json:"client_id"`
	ClientSecret string `json:"client_secret,omitempty"`
}

ClientCredentials holds OAuth client ID and secret.

type ClientCredentialsOptions added in v0.12.0

type ClientCredentialsOptions struct {
	// ClientID and ClientSecret are the confidential client the agent
	// mints its own tokens with.
	ClientID     string
	ClientSecret string

	// Scope is the access level to ask for ("read" or "full"); empty asks
	// for full, matching the device flow, since BC5 defaults an omitted
	// scope to its least-privilege entry.
	Scope string

	// Logger receives status messages during the login. Nil suppresses them.
	Logger func(msg string)

	// BeforeStore, when set, runs after the mint has proved the client and
	// before the credential is written, with the result the login is about
	// to return. A non-nil error aborts the login and nothing is stored.
	//
	// It is where a caller commits whatever must exist alongside the
	// credential — the profile entry, above all. An entry without a
	// credential is a visible, harmless state; a stored client secret under
	// a profile nothing registered is an orphan nobody will find again.
	BeforeStore func(result *LoginResult) error
}

ClientCredentialsOptions configures an agent login.

type Credentials

type Credentials struct {
	AccessToken   string `json:"access_token"`
	RefreshToken  string `json:"refresh_token"`
	ExpiresAt     int64  `json:"expires_at"`
	Scope         string `json:"scope"`
	OAuthType     string `json:"oauth_type"` // "bc5", "launchpad", "agent", or legacy "bc3"
	TokenEndpoint string `json:"token_endpoint"`

	// ClientID and ClientSecret are the confidential OAuth client an
	// "agent" credential mints its own access tokens with (see agent.go).
	// An agent principal is granted no refresh token, so these — not a
	// refresh token — are what survives an expiry, and they are stored
	// with the token in the OS keyring wherever one is available. Empty
	// for every other kind of credential.
	ClientID     string `json:"client_id,omitempty"`
	ClientSecret string `json:"client_secret,omitempty"`

	// RenewAfter is when this credential should be renewed, in Unix
	// seconds, when that is LATER than the usual RefreshWindow before
	// ExpiresAt. It exists for a token whose whole lifetime is shorter
	// than that window, which the default margin would put back inside the
	// renewal window the instant it was issued. Zero — every credential
	// today except a minted agent self-token — means the default.
	RenewAfter int64 `json:"renew_after,omitempty"`

	// RenewalHold is the token endpoint's last refusal of this
	// credential's renewal, kept so the next renewal answers it locally
	// instead of asking again (see renewal_hold.go): for an agent, a
	// refused secret or a rate limit on its mint; for anyone else, a rate
	// limit on the refresh. Nil once a renewal succeeds. Stored as
	// "mint_hold", the name it had when only an agent's mint was held, so
	// every version reads the others'.
	RenewalHold *RenewalHold `json:"mint_hold,omitempty"`

	// Issuer is the RFC 8414 issuer of the authorization server that minted
	// a BC5 credential — where its metadata, and so its revocation endpoint,
	// is found at logout. Credentials stored before it was recorded derive
	// it from TokenEndpoint (see credentialIssuer).
	Issuer    string `json:"issuer,omitempty"`
	UserID    string `json:"user_id,omitempty"`
	UserEmail string `json:"user_email,omitempty"`

	// Source records how the credential was obtained when that is not the
	// OAuth flow: "token" for an imported personal access token, which has
	// no refresh material. Empty means an OAuth login.
	Source string `json:"source,omitempty"`

	// Resource is the RFC 8707 resource indicator the tokens are bound to
	// (BC5: urn:bc:account:<id>). BC5 device logins as the trusted
	// basecamp-cli client mint MULTI-ACCOUNT refresh tokens, and the refresh
	// grant rejects them without this echo — refresh sends it when set and
	// preserves it when a refresh response omits it.
	Resource string `json:"resource,omitempty"`
}

Credentials holds OAuth tokens and metadata.

func Held added in v0.12.0

func Held(h HeldCredential) (creds *Credentials, key string, held bool)

Held reads a lock proof: the credential and key it was made with, and whether the lock is still held. Anything this package did not make — a nil, a struct embedding the interface — is reported as not held, whatever its own methods say.

type HeldCredential added in v0.12.0

type HeldCredential interface {
	// contains filtered or unexported methods
}

HeldCredential is a credential read while its key's lock is held, and the proof a write can ask for: a function that must not run without the lock takes one. Only WithCredential makes one, it stops counting when the lock is released, and Held is the only way to read it — the interface carries nothing a wrapper could answer for.

type LoginOptions

type LoginOptions struct {
	Scope     string
	NoBrowser bool // If true, don't auto-open browser, just print URL

	// Remote forces remote/headless mode: skip the loopback listener and
	// prompt the user to paste the callback URL. Auto-detected when SSH
	// env vars are present (unless Local is set).
	Remote bool

	// Local forces local mode, overriding SSH auto-detection.
	// Mutually exclusive with Remote.
	Local bool

	// LoginHint names the account (email address) the user should sign in
	// as. The device flow sends it as Basecamp's login_hint extension to the
	// device authorization request, where it steers the sign-in page and
	// never authenticates on its own; Launchpad's authorization-code flow
	// ignores it.
	LoginHint string

	// Verify, when set, is called with the freshly issued access token and
	// its provider type ("bc5" or "launchpad") before anything is stored. A
	// non-nil error aborts the login and nothing is written: the credential
	// is proven first and persisted second.
	Verify func(ctx context.Context, accessToken, oauthType string) error

	// InputReader is the source for pasted callback URLs in remote mode.
	// If nil, os.Stdin is used.
	InputReader io.Reader

	// RedirectURI overrides the OAuth redirect URI.
	// Takes precedence over BASECAMP_OAUTH_REDIRECT_URI and CallbackAddr.
	RedirectURI string

	// CallbackAddr is the address for the local OAuth callback server.
	// Default: "127.0.0.1:8976"
	CallbackAddr string

	// BrowserLauncher opens the authorization URL in a browser.
	// If nil, uses the default system browser launcher.
	BrowserLauncher func(url string) error

	// Logger receives status messages during the login flow.
	// If nil, messages are suppressed for headless/SDK use.
	Logger func(msg string)

	// Progress, when it is a terminal, carries the live wait line drawn
	// while the device flow polls for approval (spinner and expiry
	// countdown, redrawn in place). Any other writer, or nil, gets a static
	// "Waiting for approval" line through Logger instead.
	Progress io.Writer
	// contains filtered or unexported fields
}

LoginOptions configures the login flow.

type LoginResult added in v0.4.0

type LoginResult struct {
	// OAuthType is "bc5", "launchpad", or "agent" for a login that minted
	// an agent's self-token. Stored credentials may also carry legacy
	// "bc3", which no login produces.
	OAuthType string
	// Scope is the effective scope: "read"/"full" for BC5 and for an
	// agent, "" for Launchpad, which has no scopes.
	Scope string
}

LoginResult holds the outcome of a successful login — Login()'s, or LoginClientCredentials()'s. Callers use this to determine the effective scope instead of their input.

type LogoutResult added in v0.12.0

type LogoutResult struct {
	// Revoked reports that the authorization server accepted the revocation.
	Revoked bool
	// Skipped names why no revocation was attempted (one of the
	// RevokeSkipped* reasons); empty when one was.
	Skipped string
	// Err is why an attempted revocation did not go through; nil otherwise.
	Err error
	// Remaining names what Err left usable (one of the Remaining* values);
	// empty when Err is nil.
	Remaining string
	// ExpiresAt is the stored access token's expiry (Unix seconds; zero
	// when the server reported none), for saying how long RemainingAccess
	// lasts.
	ExpiresAt int64
}

LogoutResult reports what became of a credential server-side. The local copy is gone either way.

func (*LogoutResult) Outstanding added in v0.12.0

func (r *LogoutResult) Outstanding() string

Outstanding describes, for a human, what a failed revocation left usable; empty when nothing did. The access token's lifetime is the one the server reported for it, not an assumed one.

type Manager

type Manager struct {

	// Warnf receives transport-policy warnings (a proxy ignored for OAuth
	// traffic, a malformed opt-out value). Test seam; nil means stderr.
	Warnf func(format string, args ...any)
	// contains filtered or unexported fields
}

Manager handles OAuth authentication.

func NewManager

func NewManager(cfg *config.Config, httpClient *http.Client) *Manager

NewManager creates a new auth manager.

A nil httpClient is the production configuration: OAuth requests ride per-provenance clients that enforce the SDK's address policy at dial time. A non-nil httpClient is caller-owned (test-only in this codebase) and carries every OAuth request as-is — no address policy is applied on top.

func (*Manager) AccessToken

func (m *Manager) AccessToken(ctx context.Context) (string, error)

AccessToken returns a valid access token, renewing it if needed. If BASECAMP_TOKEN env var is set, it's used directly without OAuth.

func (*Manager) AccountID added in v0.9.0

func (m *Manager) AccountID() string

AccountID returns the account a BC5 token is bound to, derived from its stored RFC 8707 resource indicator, or "" when the credentials carry no account binding (Launchpad tokens, or a resource naming the service origin rather than one account).

The binding is authoritative: the token grants access to exactly this account, so there is nothing to discover over the network and no picker to show. It also works where account discovery cannot — /authorization.json is served only on the API host, which beta deployments don't route.

BASECAMP_TOKEN wins — match AccessToken() precedence. Requests carry the environment token, which is bound to whatever the operator issued it for; answering with a stored token's account would silently address the wrong one. Fall through to discovery, which asks using the token in play.

func (*Manager) AuthorizationEndpoint added in v0.7.0

func (m *Manager) AuthorizationEndpoint(ctx context.Context) (string, error)

AuthorizationEndpoint returns the authorization info endpoint URL for the current authentication context. BASECAMP_TOKEN takes precedence over stored credentials (mirroring AccessToken), with the token prefix used to determine the issuer. When no env token is set, stored OAuth type drives selection.

func (*Manager) AuthorizationEndpointFor added in v0.11.0

func (m *Manager) AuthorizationEndpointFor(oauthType string) (string, error)

AuthorizationEndpointFor returns the authorization info endpoint for a credential of the given OAuth type, whether or not it is stored yet.

func (*Manager) CheckAuthenticated added in v0.12.0

func (m *Manager) CheckAuthenticated(ctx context.Context) (bool, error)

CheckAuthenticated is IsAuthenticated for a caller that ACTS on the answer rather than printing it: whether a credential is stored, and separately why it could not tell.

A bool cannot carry "I could not reach the store", and a gate that reads that as "you are not logged in" refuses work for a credential that is sitting right there — a store lock another process holds for the length of one write, a keyring that will not open, a wait the person canceled. Anything that stops a command uses this and says which it was; the bool form stays for the reports and prompts that only need a hint.

func (*Manager) ConnectAgent added in v0.12.0

func (m *Manager) ConnectAgent(ctx context.Context, opts AgentConnectOptions) (*AgentConnectResult, error)

ConnectAgent runs the agent-connection handshake and stores what it is given as this profile's credential.

Intake, then the operator's browser trip, then the poll — and on the first successful poll, the same path an agent login takes: one client_credentials mint to prove the credential is good, the caller's BeforeStore hook, and one write. Nothing is stored before the mint succeeds, so a connection the server would not honor leaves no profile behind.

func (*Manager) CredentialKey

func (m *Manager) CredentialKey() string

CredentialKey returns the current credential storage key. This is exported for use in commands that need to display or lookup credentials.

func (*Manager) GetOAuthType

func (m *Manager) GetOAuthType() string

GetOAuthType returns the OAuth type for the current credential key ("bc5", "launchpad", or legacy "bc3").

func (*Manager) GetStore

func (m *Manager) GetStore() *Store

GetStore returns the credential store.

func (*Manager) GetUserEmail added in v0.3.0

func (m *Manager) GetUserEmail() string

GetUserEmail returns the stored user email for the current credential key.

func (*Manager) ImportToken added in v0.11.0

func (m *Manager) ImportToken(ctx context.Context, token, scope, userID, userEmail string, expiresAt time.Time) error

ImportToken stores an externally issued BC5 access token (a personal access token) as the current credential, in one write, together with the identity it was verified to authenticate as and the expiry the server reported for it (zero when it reported none). The token has no refresh token and no token endpoint: a zero expiry is the non-expiring path AccessToken already takes, and a reported one makes AccessToken refuse the token near expiry with "No refresh token available" rather than letting requests start failing — either way the remedy is to import again. Scope is what the token was verified or declared to carry.

ctx bounds the wait for the credential's cross-process lock, so a canceled import stores nothing.

func (*Manager) IsAuthenticated

func (m *Manager) IsAuthenticated() bool

IsAuthenticated reports whether a credential is stored, answering false for anything it could not read — as it has always done for an unreadable keyring. Returns true if BASECAMP_TOKEN env var is set or if OAuth credentials exist.

Use CheckAuthenticated where a false STOPS something: this one cannot tell "nothing is stored" from "I could not look".

func (*Manager) Login

func (m *Manager) Login(ctx context.Context, opts LoginOptions) (*LoginResult, error)

Login initiates the OAuth login flow. Discovery selects the provider: a BC5 issuer runs the RFC 8628 device flow; the Launchpad fallback runs the authorization-code flow with a loopback (or pasted) callback.

func (*Manager) LoginClientCredentials added in v0.12.0

func (m *Manager) LoginClientCredentials(ctx context.Context, opts ClientCredentialsOptions) (*LoginResult, error)

LoginClientCredentials authenticates as a Basecamp agent principal and stores the result under the active credential key.

It discovers the authorization server the same way an interactive login does, mints one self-token — which is what proves the client id and secret are good, since there is no person to ask — and stores the client credentials alongside the token so every later command can mint another when this one expires.

Only Basecamp's own authorization server issues these. A discovery that falls back to Launchpad is refused rather than attempted: Launchpad has no agent principals, and sending a client secret to it on the strength of a fallback would be a guess with a secret.

func (*Manager) LoginCommand added in v0.12.0

func (m *Manager) LoginCommand() string

LoginCommand is the command that re-establishes the active credential: addressed to the active profile when there is one, since a bare login would store the new credential under the base URL instead. The command is meant to be pasted, so every interpolated value is shell-quoted.

func (*Manager) LoginHint added in v0.12.0

func (m *Manager) LoginHint() string

LoginHint is LoginCommand as an error hint.

func (*Manager) Logout

func (m *Manager) Logout(ctx context.Context) (*LogoutResult, error)

Logout revokes the current credential with its authorization server when it can be, then removes it from local storage regardless. ErrNoCredential means there was nothing to remove.

func (*Manager) LogoutCredential added in v0.12.0

func (m *Manager) LogoutCredential(ctx context.Context, credKey, baseURL string) (*LogoutResult, error)

LogoutCredential is Logout for the credential stored under credKey — the path profile deletion takes for "profile:<name>". baseURL anchors the egress policy of the revocation request: the deleted profile's own base URL, which need not be the active configuration's — a loopback development profile deleted while production is active must still reach its loopback issuer. Empty means the active configuration.

func (*Manager) Refresh

func (m *Manager) Refresh(ctx context.Context) error

Refresh forces a token renewal whatever the stored expiry says. The credential is loaded under the cross-process lock, so the credential it renews is the one it saves.

func (*Manager) RefreshRefusal added in v0.12.0

func (m *Manager) RefreshRefusal(creds *Credentials) error

RefreshRefusal is the error renewing creds would fail with before anything is sent — no refresh token, a grant from the removed bc3 development flow, a missing or unusable token endpoint, a half-configured OAuth client, an agent credential missing the client that mints its tokens — or nil when a renewal would be attempted. It runs the same preparation the renewal does, on a copy, for a report that must say what the next command will do without doing it.

func (*Manager) RenewalHoldStatus added in v0.13.0

func (m *Manager) RenewalHoldStatus(creds *Credentials, refusal error) *RenewalHoldStatus

RenewalHoldStatus is the stored hold of creds as a report shows it, when refusal — RefreshRefusal's answer for the same creds — is that hold's answer; nil otherwise. It reads no clock: whether the hold is what a renewal sent now would meet was decided once, by RefreshRefusal, and a local failure checked ahead of the hold (a missing token endpoint) is not masked by it.

func (*Manager) Revoke added in v0.12.0

func (m *Manager) Revoke(ctx context.Context, creds *Credentials) error

Revoke asks the credential's authorization server to revoke it (RFC 7009) as the public basecamp-cli client: the refresh token first, which revokes its whole family, then the access token — the type hint is advisory and the second request is cheap insurance. Both ride the BC5 lane client, so the metadata and revocation endpoints are judged by the same address policy as every other BC5 request. An error means the server could not be reached, did not describe a revocation endpoint, or did not answer 200; the tokens never appear in it.

func (*Manager) RevokeStored added in v0.12.0

func (m *Manager) RevokeStored(ctx context.Context) error

RevokeStored revokes the current credential with its authorization server and, that done, removes the local copy — a revoked refresh family leaves nothing usable behind. Unlike Logout it is not best effort: a credential that could not be revoked stays stored so the operator can try again, and one that is not the CLI's to revoke (Launchpad, an imported token) is refused with the reason. ErrNoCredential means nothing is stored.

func (*Manager) SessionAccessToken added in v0.18.0

func (m *Manager) SessionAccessToken(ctx context.Context, id, label string) (string, error)

SessionAccessToken mints a fresh self-token for one agent session from the active profile's stored agent credential, and returns it without storing it. See the top of this file for why it is never cached and why a server that does not bind it to the session gets no token at all.

Holds are honored and written exactly as the shared mint's are, because they are verdicts on the client secret both mints present: a refused or rate-limited secret is answered locally here too, and a refusal of the secret seen here is remembered for every later mint. A refusal of the request itself — the session's parameters, above all — is not about the secret and holds nothing, so one malformed launch cannot stop the rest.

func (*Manager) SetClock added in v0.12.0

func (m *Manager) SetClock(clock func() time.Time)

SetClock sets the time a remembered mint verdict is read and written against, for a caller that drives time itself; nil means time.Now.

func (*Manager) SetStore added in v0.3.0

func (m *Manager) SetStore(s *Store)

SetStore replaces the credential store. Used in tests to inject a file-backed store rooted in a temp directory.

func (*Manager) SetUserEmail added in v0.3.0

func (m *Manager) SetUserEmail(ctx context.Context, email string) error

SetUserEmail stores the user email for the current credential key without modifying the stored user ID.

BASECAMP_TOKEN wins — match AccessToken() precedence. The email was fetched with the environment token, so it names that token's user, not whoever the stored credentials belong to; writing it there would mislabel them. Skipping the store also keeps a token session off the keyring probe and the fallback warning it can raise.

An empty email is an omission, not a value: an in-house (bc3) token's authorization document carries only the identity id, and a caller relaying that must not blank what a login stored.

func (*Manager) SetUserIdentity added in v0.3.0

func (m *Manager) SetUserIdentity(ctx context.Context, userID, email string) error

SetUserIdentity stores the user ID and email for the current credential key. As with SetUserEmail, an empty value leaves the stored field alone. Unlike it, BASECAMP_TOKEN does not suppress the write: a login stores its new credential and then records who it verified as, whatever the environment holds, so the caller decides whose identity this is.

func (*Manager) StoredAccessToken

func (m *Manager) StoredAccessToken(ctx context.Context) (string, error)

StoredAccessToken returns a valid access token from the credential store, renewing it if needed. Unlike AccessToken, this ignores the BASECAMP_TOKEN environment variable and always uses stored OAuth credentials.

type RenewalHold added in v0.13.0

type RenewalHold struct {
	// Kind is renewalHoldRefused or renewalHoldRateLimited.
	Kind string `json:"kind"`

	// Detail is what the server said, as agentMintRefusal rendered it:
	// "token error: invalid_client", or "the server answered HTTP 401".
	Detail string `json:"detail"`

	// Client is renewalSubject of the credential the verdict was about:
	// an agent's client credentials, or a refresh token. A hold for any
	// others holds nothing.
	Client string `json:"client"`

	// Until is when the next renewal may be sent, in Unix seconds, rounded
	// up. Zero on an invalid_client refusal means never with these client
	// credentials; any other hold without one is damaged, and holds nothing.
	Until int64 `json:"until,omitempty"`
}

RenewalHold is the token endpoint's last refusal of a credential's renewal — an agent's mint, or anyone else's refresh — remembered so the next renewal can answer it without asking again. A refresh is held for a rate limit (holdRateLimitedRefresh) or for an invalid_client refusal of the client it was sent as (holdRefusedRefreshClient).

Nothing in it came from the server as free text: Detail is the same fixed vocabulary agentMintRefusal renders (see oauthErrorCodes).

type RenewalHoldStatus added in v0.13.0

type RenewalHoldStatus struct {
	// Kind is "refused" or "rate_limited".
	Kind string `json:"kind"`

	// Detail is what the server said: "token error: invalid_client", or
	// "the server answered HTTP 401".
	Detail string `json:"detail"`

	// Until is when the hold ends, RFC 3339 in UTC. Empty for a hold that
	// never ends with these client credentials.
	Until string `json:"until,omitempty"`

	// Message and Hint are the error a mint held now would return.
	Message string `json:"message"`
	Hint    string `json:"hint,omitempty"`
}

RenewalHoldStatus is a stored mint hold as a report shows it: what would happen to a renewal sent now.

func (*RenewalHoldStatus) Permanent added in v0.13.0

func (s *RenewalHoldStatus) Permanent() bool

Permanent reports whether the hold never ends with these client credentials — only a login with a new secret replaces it.

type Store

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

Store wraps credstore.Store with typed Credentials marshaling.

Construction of the underlying credstore.Store is deferred to first use: credstore.NewStore probes the OS keyring availability with a write, and on a locked keychain with no TTY or GUI (headless macOS) that probe blocks forever in an uncancellable `security` child process. Credential-free commands must never pay it.

func NewStore

func NewStore(fallbackDir string) *Store

NewStore creates a credential store. The OS keyring is not touched until the first credential operation.

func (*Store) Delete

func (s *Store) Delete(origin string) error

Delete removes credentials for the given origin. Locked for the same reason Save is: the file backend rewrites the whole document.

func (*Store) Load

func (s *Store) Load(origin string) (*Credentials, error)

Load retrieves credentials for the given origin.

The read takes a SHARED store lock on the file backend (see lock.go): readers do not queue behind each other, but none of them can land in the middle of a writer's replacement of credentials.json.

func (*Store) LoadContext added in v0.12.0

func (s *Store) LoadContext(ctx context.Context, origin string) (*Credentials, error)

LoadContext is Load for a caller that can be canceled: the wait for the store lock ends when ctx does. Every credential operation with a command behind it uses this, so a stopped command stops waiting.

func (*Store) MigrateToKeyring

func (s *Store) MigrateToKeyring() error

MigrateToKeyring migrates credentials from file to keyring. It reads credentials.json, saves every key it finds into the keyring, and removes the file, so it is a whole-store read-modify-write and takes the store lock — unconditionally, since it does its file work precisely when the keyring is in use and the ordinary store lock steps aside.

The lock keeps it exclusive with any file-backed save, which is the race that could lose a credential outright (read the file, another process adds a key, remove the file). It does not serialize the per-key writes against a concurrent refresh of one of those keys: that would mean taking every key's lock underneath the store lock, inverting the order the rest of this package holds them in. A refresh landing inside a migration can therefore be re-saved from the file copy — one stale credential, one login to repair, against a deadlock in the common path.

func (*Store) Save

func (s *Store) Save(origin string, creds *Credentials) error

Save stores credentials for the given origin.

The write is taken under the whole-store lock: the file backend keeps every key in one credentials.json and rewrites the entire document, so two processes saving DIFFERENT keys at once would otherwise drop one of them. See lock.go.

func (*Store) SaveContext added in v0.12.0

func (s *Store) SaveContext(ctx context.Context, origin string, creds *Credentials) error

SaveContext is Save for a caller that can be canceled: the wait for the store lock ends when ctx does, and a cancellation that lands while waiting abandons the write rather than making it the moment the lock frees up. A login uses it so a Ctrl-C between its last check and the write cannot store the credential the person stopped.

func (*Store) UsingKeyring

func (s *Store) UsingKeyring() bool

UsingKeyring returns true if the store is using the system keyring.

func (*Store) WithCredential added in v0.12.0

func (s *Store) WithCredential(ctx context.Context, key string, fn func(HeldCredential) error) error

WithCredential runs fn with one key's credential while holding that key's cross-process lock, so no other process's login, refresh, import or logout for the key can land while fn runs. A caller that must act on a credential being what it just read — writing a file that names the identity it authenticates as, say — does its read, its decision and its write inside fn.

A host that cannot lock at all is an error here, not a warning: the callers that reach for this are the ones whose guarantee is the lock.

fn must not take the same key's lock again — no AccessToken, no StoredAccessToken, no SetUserIdentity, no nested WithCredential. Locks are per open file description, so a second acquire in this process waits on itself until the wait bound expires. fn must not make a network request either: every other process's work on the key queues behind it.

A key with nothing stored is reported as ErrNoCredential, never as a nil credential.

Jump to

Keyboard shortcuts

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