Documentation
¶
Overview ¶
Package auth provides OAuth 2.1 authentication for Basecamp.
Index ¶
- Constants
- type ClientCredentials
- type Credentials
- type LoginOptions
- type LoginResult
- 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) CredentialKey() string
- func (m *Manager) GetOAuthType() string
- func (m *Manager) GetStore() *Store
- func (m *Manager) GetUserEmail() string
- func (m *Manager) ImportToken(token, scope, userID, userEmail string, expiresAt time.Time) error
- func (m *Manager) IsAuthenticated() bool
- func (m *Manager) Login(ctx context.Context, opts LoginOptions) (*LoginResult, error)
- func (m *Manager) Logout() error
- func (m *Manager) Refresh(ctx context.Context) error
- func (m *Manager) SetStore(s *Store)
- func (m *Manager) SetUserEmail(email string) error
- func (m *Manager) SetUserIdentity(userID, email string) error
- func (m *Manager) StoredAccessToken(ctx context.Context) (string, error)
- type Store
Constants ¶
const CredentialSourceToken = "token"
CredentialSourceToken marks a credential imported from a personal access token rather than obtained through an OAuth flow.
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 ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ClientCredentials ¶
type ClientCredentials struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
}
ClientCredentials holds OAuth client ID and secret.
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", or legacy "bc3"
TokenEndpoint string `json:"token_endpoint"`
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.
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)
// contains filtered or unexported fields
}
LoginOptions configures the login flow.
type LoginResult ¶ added in v0.4.0
type LoginResult struct {
OAuthType string // "bc5" or "launchpad" (stored credentials may also carry legacy "bc3")
Scope string // effective scope: "read"/"full" for BC5, "" for Launchpad
}
LoginResult holds the outcome of a successful Login(). Callers use this to determine the effective scope instead of their input.
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, refreshing 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) 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
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.
func (*Manager) IsAuthenticated ¶
IsAuthenticated checks if there are valid credentials. Returns true if BASECAMP_TOKEN env var is set or if OAuth credentials exist.
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) 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.
func (*Manager) SetUserIdentity ¶ added in v0.3.0
SetUserIdentity stores the user ID and email for the current credential key.
func (*Manager) StoredAccessToken ¶
StoredAccessToken returns a valid access token from the credential store, refreshing if needed. Unlike AccessToken, this ignores the BASECAMP_TOKEN environment variable and always uses stored OAuth credentials.
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) Load ¶
func (s *Store) Load(origin string) (*Credentials, error)
Load retrieves credentials for the given origin.
func (*Store) MigrateToKeyring ¶
MigrateToKeyring migrates credentials from file to keyring.
func (*Store) Save ¶
func (s *Store) Save(origin string, creds *Credentials) error
Save stores credentials for the given origin.
func (*Store) UsingKeyring ¶
UsingKeyring returns true if the store is using the system keyring.