Documentation
¶
Overview ¶
Package auth provides OAuth 2.1 authentication for Basecamp.
Index ¶
- Constants
- Variables
- func NamedWait(err error) int
- func ProfileCredentialKey(profile string) string
- func RetryAfter(err error) int
- type AgentConnectOptions
- type AgentConnectResult
- type AgentConnection
- type ClientCredentials
- type ClientCredentialsOptions
- type Credentials
- type HeldCredential
- type LoginOptions
- type LoginResult
- type LogoutResult
- type Manager
- func (m *Manager) AccessToken(ctx context.Context) (string, error)
- func (m *Manager) AccountID() string
- func (m *Manager) AuthorizationEndpoint(ctx context.Context) (string, error)
- func (m *Manager) AuthorizationEndpointFor(oauthType string) (string, error)
- func (m *Manager) CheckAuthenticated(ctx context.Context) (bool, error)
- func (m *Manager) ConnectAgent(ctx context.Context, opts AgentConnectOptions) (*AgentConnectResult, error)
- func (m *Manager) CredentialKey() string
- func (m *Manager) GetOAuthType() string
- func (m *Manager) GetStore() *Store
- func (m *Manager) GetUserEmail() string
- func (m *Manager) ImportToken(ctx context.Context, token, scope, userID, userEmail string, ...) error
- func (m *Manager) IsAuthenticated() bool
- func (m *Manager) Login(ctx context.Context, opts LoginOptions) (*LoginResult, error)
- func (m *Manager) LoginClientCredentials(ctx context.Context, opts ClientCredentialsOptions) (*LoginResult, error)
- func (m *Manager) LoginCommand() string
- func (m *Manager) LoginHint() string
- func (m *Manager) Logout(ctx context.Context) (*LogoutResult, error)
- func (m *Manager) LogoutCredential(ctx context.Context, credKey, baseURL string) (*LogoutResult, error)
- func (m *Manager) MintHoldStatus(creds *Credentials, refusal error) *MintHoldStatus
- func (m *Manager) Refresh(ctx context.Context) error
- func (m *Manager) RefreshRefusal(creds *Credentials) error
- func (m *Manager) Revoke(ctx context.Context, creds *Credentials) error
- func (m *Manager) RevokeStored(ctx context.Context) error
- func (m *Manager) SetClock(clock func() time.Time)
- func (m *Manager) SetStore(s *Store)
- func (m *Manager) SetUserEmail(ctx context.Context, email string) error
- func (m *Manager) SetUserIdentity(ctx context.Context, userID, email string) error
- func (m *Manager) StoredAccessToken(ctx context.Context) (string, error)
- type MintHold
- type MintHoldStatus
- type Store
- func (s *Store) Delete(origin string) error
- func (s *Store) Load(origin string) (*Credentials, error)
- func (s *Store) LoadContext(ctx context.Context, origin string) (*Credentials, error)
- func (s *Store) MigrateToKeyring() error
- func (s *Store) Save(origin string, creds *Credentials) error
- func (s *Store) SaveContext(ctx context.Context, origin string, creds *Credentials) error
- func (s *Store) UsingKeyring() bool
- func (s *Store) WithCredential(ctx context.Context, key string, fn func(HeldCredential) error) error
Constants ¶
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. Ending an agent's // access means rotating the client secret in Basecamp. RevokeSkippedAgent = "agent" )
Reasons a credential is removed locally without a revocation attempt.
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.
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.
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.
const CredentialSourceToken = "token"
CredentialSourceToken marks a credential imported from a personal access token rather than obtained through an OAuth flow.
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.
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 ¶
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: the agent was disconnected in Basecamp, or connected on another computer, which replaced its secret.
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.
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
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
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
RetryAfter is the wait, in seconds, a rate-limited answer named, or zero when it named none.
Types ¶
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
// 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
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"`
// MintHold is the token endpoint's last refusal of an agent
// credential's client — a refused secret, or a rate limit — kept so
// the next mint answers it locally instead of asking again (see
// agent_hold.go). Nil for every other kind of credential, and for an
// agent whose last mint succeeded.
MintHold *MintHold `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 ¶
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 ¶
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
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
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
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
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 ¶
CredentialKey returns the current credential storage key. This is exported for use in commands that need to display or lookup credentials.
func (*Manager) GetOAuthType ¶
GetOAuthType returns the OAuth type for the current credential key ("bc5", "launchpad", or legacy "bc3").
func (*Manager) GetUserEmail ¶ added in v0.3.0
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 ¶
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
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) 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) MintHoldStatus ¶ added in v0.12.0
func (m *Manager) MintHoldStatus(creds *Credentials, refusal error) *MintHoldStatus
MintHoldStatus 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) Refresh ¶
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) 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
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) SetClock ¶ added in v0.12.0
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
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
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
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 ¶
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 MintHold ¶ added in v0.12.0
type MintHold struct {
// Kind is mintHoldRefused or mintHoldRateLimited.
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 agentClientFingerprint of the client credentials the
// verdict was about. A hold for any others holds nothing.
Client string `json:"client"`
// Until is when the next mint 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"`
}
MintHold is the token endpoint's last refusal of an agent credential's client, remembered so the next mint can answer it without asking again.
Nothing in it came from the server as free text: Detail is the same fixed vocabulary agentMintRefusal renders (see oauthErrorCodes).
type MintHoldStatus ¶ added in v0.12.0
type MintHoldStatus 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"`
}
MintHoldStatus is a stored mint hold as a report shows it: what would happen to a renewal sent now.
func (*MintHoldStatus) Permanent ¶ added in v0.12.0
func (s *MintHoldStatus) 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 ¶
NewStore creates a credential store. The OS keyring is not touched until the first credential operation.
func (*Store) Delete ¶
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
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 ¶
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
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 ¶
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.