Documentation
¶
Overview ¶
Package auth owns the CLI's credentials: how they are obtained, where they are kept, and when they are replaced.
The platform issues two tokens and this package handles both:
account token from Keycloak, says who you are, renewed with a refresh token project token from IAM, says who you are and where, obtained by exchange
Project tokens are not renewable by design. One asserts that you are still a member of that project, and membership changes; renewal would extend the claim without re-proving it.
Index ¶
- Constants
- Variables
- func OpenBrowser(target string)
- type Credentials
- type DeviceCode
- type Manager
- func (m *Manager) InvalidateProject() error
- func (m *Manager) Login(ctx context.Context, notify func(target string), abort <-chan struct{}) (*Credentials, error)
- func (m *Manager) LoginWithDeviceCode(ctx context.Context) (*DeviceCode, func() (*Credentials, error), error)
- func (m *Manager) LoginWithRefreshToken(ctx context.Context, refreshToken string) (*Credentials, error)
- func (m *Manager) Logout(ctx context.Context) error
- func (m *Manager) Status() (*Status, error)
- func (m *Manager) Token(ctx context.Context, kind spec.Credential) (string, error)
- func (m *Manager) UseExchangePath(path string)
- type Status
- type StorageMode
- type Store
Constants ¶
const EnvToken = "LEAFLOW_TOKEN"
EnvToken carries a project token straight from the environment. It bypasses storage, renewal and exchange, because CI has no browser and cannot run an interactive login.
const ExchangeOperation = "exchange-project-token"
ExchangeOperation is the contract's id for minting a project token.
Variables ¶
var ( ErrNeedProject = errors.New("no project selected") // ErrAccountTokenInCI is separate because retrying cannot help: the value in // LEAFLOW_TOKEN is a project token and the operation needs an account one. ErrAccountTokenInCI = errors.New( "this command needs an account token (register, list projects, exchange), which " + EnvToken + " cannot provide; run leaflow login") ErrNotAMember = errors.New("cannot obtain a token for that project; you may not be a member") ErrExchangeFailed = errors.New("project token exchange failed") // ErrLoginAborted means the caller gave up on the browser flow, not that // anything failed. ErrLoginAborted = errors.New("browser sign-in abandoned") ErrExchangeUnknown = errors.New( "the bundled contract does not declare " + ExchangeOperation + ", so a project token cannot be obtained") ErrTokenRejected = errors.New("the realm rejected that refresh token; it may be expired, revoked, or from another realm") )
var ( ErrDiscovery = errors.New("cannot reach the login service") // ErrNoDeviceFlow names the thing an operator has to change, because // otherwise this reads like a bug in the CLI. ErrNoDeviceFlow = errors.New( "this realm has no device authorization grant; enable OAuth 2.0 Device Authorization Grant on the client") ErrUnknownClient = errors.New("the login service does not recognise this client; it must be a public client with the device flow enabled") ErrLoginTimeout = errors.New("login timed out, run leaflow login again") ErrLoginDenied = errors.New("login was denied") ErrDeviceCodeExpired = errors.New("device code expired, run leaflow login again") )
var ( ErrAuthorizationDenied = errors.New("authorization was denied") // ErrStateMismatch means the callback did not belong to this login attempt. // Continuing anyway is how an authorization code from another origin gets // accepted, so it is fatal rather than a retry. ErrStateMismatch = errors.New("callback state did not match") )
var ( // ErrNoCredentials means this context has never logged in, or its refresh // token is gone. Callers turn it into "run leaflow login" rather than // surfacing a keyring failure. ErrNoCredentials = errors.New("not logged in") ErrCredentialsUnwritable = errors.New("cannot store credentials") ErrCredentialsUnreadable = errors.New("cannot read credentials") ErrNoKeychain = errors.New("no usable system keychain") )
var StorageModes = []string{string(StorageAuto), string(StorageKeychain), string(StorageFile)}
Functions ¶
func OpenBrowser ¶
func OpenBrowser(target string)
OpenBrowser is best effort and reports nothing: the URL is already on screen, and a "could not open browser" error would read as though login had failed.
Types ¶
type Credentials ¶
type Credentials struct {
AccountToken string `json:"account_token,omitempty"`
AccountExpires time.Time `json:"account_expires,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Account string `json:"account,omitempty"`
// Offline records that the refresh token is an offline token, which is not
// tied to an SSO session and so does not expire when that one does.
Offline bool `json:"offline,omitempty"`
ProjectToken string `json:"project_token,omitempty"`
ProjectExpires time.Time `json:"project_expires,omitempty"`
// ProjectID records which project the cached token belongs to. Switching
// projects without clearing it would not fail — it would succeed against the
// wrong project, which is worse than any error.
ProjectID string `json:"project_id,omitempty"`
}
Credentials is one context's full credential set. Keeping them per context matters because working against production and a local stack at once is normal, and mixing the two only ever shows up as a 401.
type DeviceCode ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the single entry point for "give me a usable token". Commands ask for one and get renewal, exchange, or a clear "log in" without each having to remember to check.
func (*Manager) InvalidateProject ¶
InvalidateProject drops the cached project token. Switching projects without it means the next command runs against the previous project and succeeds.
func (*Manager) Login ¶
func (m *Manager) Login(ctx context.Context, notify func(target string), abort <-chan struct{}) (*Credentials, error)
Login signs in through the browser using authorization code with PKCE.
A public client has no secret to prove it is itself, so PKCE supplies one per attempt: the verifier stays in this process and only its SHA-256 hash is sent with the request, which makes an intercepted code unusable. The realm requires S256 for this client, so a downgrade to `plain` is refused.
The redirect is a loopback listener on a port the OS picks, per RFC 8252. abort, when closed, gives up on the browser flow and returns ErrLoginAborted. The caller offers it when a keypress should switch to the device flow instead.
func (*Manager) LoginWithDeviceCode ¶
func (m *Manager) LoginWithDeviceCode(ctx context.Context) (*DeviceCode, func() (*Credentials, error), error)
LoginWithDeviceCode signs in without a browser on this machine.
Needed wherever a loopback redirect cannot arrive: over ssh, inside a container, on a headless host. The two halves of the flow may happen on different machines, which is the whole point of RFC 8628.
PKCE applies here too. It is not part of RFC 8628, but Keycloak implements the extension and this client requires it, so the verifier is carried through to redemption exactly as in the browser flow.
func (*Manager) LoginWithRefreshToken ¶
func (m *Manager) LoginWithRefreshToken(ctx context.Context, refreshToken string) (*Credentials, error)
LoginWithRefreshToken signs in from a refresh token read elsewhere, for CI.
It redeems the token immediately rather than storing it as given: that proves it is valid and belongs to this realm, so a bad value fails at login instead of at the first real command, where it would look like an outage.
A refresh token is what CI wants because it is the only credential this CLI can renew on its own. A project token expires in minutes, which is what LEAFLOW_TOKEN is for.
func (*Manager) Status ¶
Status reads state without renewing or exchanging: a command for inspecting state should not change it.
func (*Manager) UseExchangePath ¶
UseExchangePath tells the manager where the exchange lives. The CLI wires this from the loaded contract at startup.
type Status ¶
type Status struct {
LoggedIn bool
Account string
AccountExpires time.Time
Project string
ProjectExpires time.Time
FromEnv bool
Storage string
// Offline reports whether the stored credential outlives the SSO session.
// Worth surfacing: it is the difference between signing in twice a day and
// signing in once a month.
Offline bool
}
type StorageMode ¶
type StorageMode string
StorageMode says where credentials are kept.
The system keychain is the default on all three platforms, reached through the OS API directly — macOS Keychain, Windows Credential Manager, and the freedesktop Secret Service on Linux — so nothing extra has to be installed.
It is not always available: CI containers, machines reached over ssh, and Linux without a session bus have no keychain. StorageFile is for those, and choosing it explicitly is better than discovering the fallback by accident.
const ( // StorageAuto uses the keychain and falls back to a file. StorageAuto StorageMode = "auto" // StorageKeychain refuses to fall back. Worth setting where a credential // silently landing in a file would be a problem worth failing over. StorageKeychain StorageMode = "keychain" // StorageFile always writes a 0600 file and never touches the keychain, // which also avoids the OS prompting for access on every run. StorageFile StorageMode = "file" )
func ParseStorageMode ¶
func ParseStorageMode(value string) (StorageMode, error)
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) Clear ¶
Clear removes both copies regardless of mode. Logging out has to be complete, including a file left behind by an earlier run under a different setting.
func (*Store) Load ¶
func (s *Store) Load() (*Credentials, error)
func (*Store) Save ¶
func (s *Store) Save(creds *Credentials) error