Documentation
¶
Index ¶
Constants ¶
const DefaultClientID = "appwrite-cli"
DefaultClientID is the OAuth2 client the CLI identifies as.
const OAuth2Scopes = "openid email profile all"
OAuth2Scopes are the scopes the CLI requests.
Variables ¶
var ErrDeviceAuthorizationExpired = errors.New("device authorization expired before it was approved")
ErrDeviceAuthorizationExpired means the user did not approve in time.
var ErrSessionExpired error = sessionExpired{}
ErrSessionExpired is returned when no usable credential remains.
var Trace func(format string, arguments ...any)
Trace receives one line per credential-store read when --verbose is on.
A package variable for the same reason client.RequestLog is one: the store is reached deep inside the auth path and diagnostics should not have to be threaded through every caller. Set during start-up, before any read.
var Warn func(format string, arguments ...any)
Warn receives a message the user should see whether or not --verbose is on.
Separate from Trace because falling back to plaintext is not diagnostics: it changes where the user's refresh token is stored, and staying quiet about it meant a headless Linux box or a locked keychain wrote a long-lived credential to disk with nothing said. Wired the same way Trace is, for the same reason.
Functions ¶
func DecodeIDToken ¶
DecodeIDToken pulls the profile claims out of an OIDC ID token.
The signature is not verified: the token arrived over TLS from the endpoint the user just authenticated against, and it is used only to label the stored session. Ports decodeIdToken().
Types ¶
type Authenticator ¶
type Authenticator struct {
Global *config.Global
Store *TokenStore
SDKVersion string
// Now is injectable so expiry logic is testable without sleeping.
Now func() time.Time
}
Authenticator resolves a usable access token for the active session.
func NewAuthenticator ¶
func NewAuthenticator(global *config.Global, sdkVersion string) *Authenticator
NewAuthenticator wires an authenticator over the given preferences.
func (*Authenticator) AccessToken ¶
func (a *Authenticator) AccessToken(forceRefresh bool) (string, error)
AccessToken returns a valid access token, refreshing it when necessary.
forceRefresh renews even a token that still looks valid, which the caller uses after the API rejects one.
type DeviceAuthorization ¶
type DeviceAuthorization struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn json.Number `json:"expires_in"`
Interval json.Number `json:"interval"`
}
DeviceAuthorization is the server's response to a device authorization request.
func (DeviceAuthorization) Lifetime ¶
func (d DeviceAuthorization) Lifetime() time.Duration
Lifetime is how long the authorization is valid for.
An explicit zero means already expired and is honoured as such, ending the loop immediately. Only a missing or unparseable value falls back to a default, which stops a malformed response becoming an instant failure.
func (DeviceAuthorization) PollInterval ¶
func (d DeviceAuthorization) PollInterval() time.Duration
PollInterval is how long to wait between token requests.
func (DeviceAuthorization) VerificationURL ¶
func (d DeviceAuthorization) VerificationURL() string
VerificationURL is the URL to show the user, preferring the one that embeds the code so they do not have to type it.
type DeviceFlow ¶
type DeviceFlow struct {
Endpoint string
ClientID string
SDKVersion string
// SelfSigned accepts a self-signed certificate on the endpoint, for a
// self-hosted instance behind its own. Set from the stored preference by the
// caller, which is the only place that reads preferences.
SelfSigned bool
// Sleep and Now are injectable so the poll loop is testable without
// waiting real seconds.
Sleep func(time.Duration)
Now func() time.Time
}
DeviceFlow runs RFC 8628 device authorization against one endpoint.
func NewDeviceFlow ¶
func NewDeviceFlow(endpoint, sdkVersion string) *DeviceFlow
NewDeviceFlow wires a device flow against an endpoint.
func (*DeviceFlow) Authorize ¶
func (f *DeviceFlow) Authorize() (DeviceAuthorization, error)
Authorize requests a device code and user code.
func (*DeviceFlow) Poll ¶
func (f *DeviceFlow) Poll(authorization DeviceAuthorization) (*TokenSet, error)
Poll requests a token until the user approves, the authorization expires, or the server returns a real error.
Returns ErrDeviceAuthorizationExpired when the window closes. Pending and empty-body responses are retried; `slow_down` additionally widens the interval per RFC 8628 section 3.5.
type MissingRefreshToken ¶
type MissingRefreshToken struct {
SessionID string
Store string
// StoreErr is what the credential store said. A not-found error means it
// answered; anything else means it failed, which is worth wording
// differently because unlocking a keyring and signing in again are
// different actions.
StoreErr error
PrefsPath string
}
MissingRefreshToken reports that a session has no refresh token, and says where the CLI looked.
The where is the point: "session expired" is a conclusion, and it is wrong whenever the CLI is looking somewhere other than where the token is -- a redirected HOME points macOS at a different login keychain and a good session reads as expired. That case cannot be told apart from a genuinely absent entry by the error alone, since `security` exits 44 for both, so naming the store and session id is the only honest thing available.
func (*MissingRefreshToken) Error ¶
func (e *MissingRefreshToken) Error() string
func (*MissingRefreshToken) Unwrap ¶
func (e *MissingRefreshToken) Unwrap() error
Unwrap exposes the store's own error, so a caller can still match on it.
type SessionRejectedError ¶
type SessionRejectedError struct {
Session string
// contains filtered or unexported fields
}
SessionRejectedError reports a refresh token the server refused.
It says rejected rather than expired because invalid_grant covers a session revoked elsewhere and a rotation the CLI lost a race on as well as a genuine expiry, and expiry is the one of the three the CLI cannot confirm. It names the session for the reason cannotRefresh does: preferences hold one per environment, and "the session" identifies none of them.
The server's own description stays in the unwrap chain rather than the sentence. On this endpoint it is a fixed string -- "Invalid refresh token provided." -- that only restates the rejection, and --verbose prints it.
func (*SessionRejectedError) Error ¶
func (e *SessionRejectedError) Error() string
func (*SessionRejectedError) Is ¶
func (e *SessionRejectedError) Is(target error) bool
func (*SessionRejectedError) Unwrap ¶
func (e *SessionRejectedError) Unwrap() error
type TokenStore ¶
TokenStore reads and writes refresh tokens, preferring the OS keyring and falling back to the preferences file.
The fallback is not a convenience: headless Linux and CI containers have no secret service, and a CLI that refuses to hold a session there is a CLI that cannot be scripted.
func (*TokenStore) DeleteRefresh ¶
func (s *TokenStore) DeleteRefresh(sessionID string) error
DeleteRefresh removes a refresh token from both stores.
A missing or unavailable keyring must not block local cleanup, so the keyring error is ignored and the prefs copy is removed regardless.
func (*TokenStore) Refresh ¶
func (s *TokenStore) Refresh(sessionID string) (string, error)
Refresh returns the stored refresh token for a session.
The failed lookup is described rather than reduced to "": "no token here" and "the store would not answer" are different advice. See MissingRefreshToken.
A token in prefs wins over the store's opinion -- it is the fallback SetRefresh writes to when the keyring is unavailable, so the store not answering does not matter.
func (*TokenStore) SetRefresh ¶
func (s *TokenStore) SetRefresh(sessionID, token string) error
SetRefresh stores a refresh token, preferring the keyring.
On success the prefs copy is removed, so a token never lingers in plaintext after the keyring starts working. On failure it is written to prefs instead, and the user is told -- the fallback is the documented behaviour for headless Linux and CI, but which store holds a long-lived credential is theirs to know.