Documentation
¶
Overview ¶
Package oauth implements the Nimbu CLI's OAuth 2.0 client: authorization server discovery, the loopback (PKCE) and device login flows, token revocation, and a persistent token source that refreshes the stored session.
Index ¶
- Constants
- Variables
- func ClientID() string
- func Credential(tok *oauth2.Token, clientID string) auth.Credential
- func IsInvalidGrant(err error) bool
- func LockPath(dir, host string) string
- func LockSession(ctx context.Context, lockPath string) (func(), error)
- type BrowserLogin
- type Client
- func (c *Client) Endpoints(ctx context.Context) Endpoints
- func (c *Client) LoginWithBrowser(ctx context.Context, opts BrowserLogin) (*oauth2.Token, error)
- func (c *Client) LoginWithDevice(ctx context.Context, opts DeviceLogin) (*oauth2.Token, error)
- func (c *Client) Refresh(ctx context.Context, refreshToken string) (*oauth2.Token, error)
- func (c *Client) Revoke(ctx context.Context, token, hint string) error
- type DeviceLogin
- type Endpoints
- type RefreshError
- type Store
- type TokenSource
Constants ¶
const ClientIDEnv = "NIMBU_OAUTH_CLIENT_ID"
ClientIDEnv overrides the client id, for development servers only.
const DefaultBrowserTimeout = 5 * time.Minute
DefaultBrowserTimeout bounds how long the loopback flow waits for the user.
const DefaultClientID = "nimbu-cli"
DefaultClientID is the first-party public client registered for the CLI.
const ExpirySkew = 5 * time.Minute
ExpirySkew is how close to expiry an access token the API rejected may be for the rejection to count as an expiry. It absorbs clock skew between this machine and the server.
const RefreshMargin = 60 * time.Second
RefreshMargin is how long before expiry an access token is renewed.
Variables ¶
var ( // ErrDeviceDenied means the user denied the device login in the browser. ErrDeviceDenied = errors.New("the login request was denied in the browser") // ErrDeviceExpired means the user code expired before it was approved. ErrDeviceExpired = errors.New("the login code expired before it was approved") )
var ErrLoginTimeout = errors.New("timed out waiting for the browser login to finish")
ErrLoginTimeout means the user did not finish the browser login in time.
var ErrSessionExpired = fmt.Errorf("session expired: %w", auth.ErrNoToken)
ErrSessionExpired means the stored session can no longer be refreshed and the user has to log in again. It wraps auth.ErrNoToken so callers treat it as "not logged in".
Functions ¶
func Credential ¶
func Credential(tok *oauth2.Token, clientID string) auth.Credential
Credential converts a token response into a stored credential.
func IsInvalidGrant ¶
IsInvalidGrant reports whether the server rejected a grant as invalid, expired or revoked.
func LockSession ¶
LockSession takes the cross-process lock that guards the session stored for one host; the returned func releases it. Anything that replaces or deletes that session (refresh, login, logout) holds it, so a refresh never writes back over a newer login. An empty lockPath, or a lock file that cannot be used, skips the lock rather than blocking the command.
Types ¶
type BrowserLogin ¶
type BrowserLogin struct {
Scopes []string
DeviceName string
Timeout time.Duration
// Announce receives the authorization URL before the browser opens, so
// the user can open it by hand when no browser starts.
Announce func(authURL string)
// Open launches the browser. Failures are ignored: the URL was announced.
Open func(authURL string) error
}
BrowserLogin configures the loopback authorization code flow (RFC 8252).
type Client ¶
type Client struct {
APIURL string
ClientID string
HTTPClient *http.Client
// contains filtered or unexported fields
}
Client speaks OAuth to the authorization server that fronts one Nimbu API.
func NewClient ¶
NewClient returns a client for the API at apiURL. The client id defaults to DefaultClientID unless ClientIDEnv is set.
func (*Client) Endpoints ¶
Endpoints discovers the server metadata once (RFC 8414) and falls back to the conventional Nimbu URLs when discovery is unavailable.
func (*Client) LoginWithBrowser ¶
LoginWithBrowser runs the authorization code flow with PKCE (S256) against a one-shot callback server on 127.0.0.1 and exchanges the code for tokens.
func (*Client) LoginWithDevice ¶
LoginWithDevice requests a user code, shows it through opts.Prompt and polls the token endpoint until the user approves, denies or the code expires.
type DeviceLogin ¶
type DeviceLogin struct {
Scopes []string
DeviceName string
// Prompt shows the user where to go and which code to enter. The code is
// never embedded in a link: typing it by hand is the phishing guard.
Prompt func(verificationURI, userCode string, expiresAt time.Time)
}
DeviceLogin configures the device authorization flow (RFC 8628).
type Endpoints ¶
type Endpoints struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
}
Endpoints are the authorization server URLs the CLI talks to.
func FallbackEndpoints ¶
FallbackEndpoints derives the Nimbu endpoints from the API URL: the token, device and revocation endpoints live on the API host, and the browser consent screen on the www host of the same domain (api.X -> www.X).
type RefreshError ¶
type RefreshError struct {
Status int // HTTP status of the token response
Code string // OAuth error code; empty when the server sent none
}
RefreshError is a refresh the token endpoint answered with anything but a new session or invalid_grant. The stored session is kept: the server did not say it is gone.
func (*RefreshError) Error ¶
func (e *RefreshError) Error() string
func (*RefreshError) Rejected ¶
func (e *RefreshError) Rejected() bool
Rejected reports whether the server refused this client or session (invalid_client, unauthorized_client, ...), so retrying cannot help and the user has to log in again. Otherwise the failure is on the server side and may pass.
type Store ¶
type Store interface {
GetCredential() (auth.Credential, error)
SetCredential(cred auth.Credential) error
DeleteCredential() error
}
Store persists the session. GetCredential must read the backing store rather than a cache: a refresh re-reads it to pick up a rotation another process already made.
type TokenSource ¶
type TokenSource struct {
// contains filtered or unexported fields
}
TokenSource hands out the access token of a stored OAuth session and renews it with the rotating refresh token. It is safe for concurrent use: refreshes are single-flight within the process and serialised across processes with a lock file, because the server treats a reused refresh token as theft and revokes the whole session.
func NewTokenSource ¶
func NewTokenSource(client *Client, store Store, cred auth.Credential, lockPath string) *TokenSource
NewTokenSource returns a token source for cred. lockPath may be empty to skip cross-process locking.
func (*TokenSource) MaybeExpired ¶
func (s *TokenSource) MaybeExpired(token string) bool
MaybeExpired reports whether the API may have rejected token because it expired: it is no longer the current token, its expiry is unknown, or it expires within ExpirySkew. A 401 for a token with time left is about something else (e.g. a customer login's wrong password) and must not trigger a refresh and replay.