oauth

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 20 Imported by: 0

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

View Source
const ClientIDEnv = "NIMBU_OAUTH_CLIENT_ID"

ClientIDEnv overrides the client id, for development servers only.

View Source
const DefaultBrowserTimeout = 5 * time.Minute

DefaultBrowserTimeout bounds how long the loopback flow waits for the user.

View Source
const DefaultClientID = "nimbu-cli"

DefaultClientID is the first-party public client registered for the CLI.

View Source
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.

View Source
const RefreshMargin = 60 * time.Second

RefreshMargin is how long before expiry an access token is renewed.

Variables

View Source
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")
)
View Source
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.

View Source
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 ClientID

func ClientID() string

ClientID returns the OAuth client id the CLI identifies as.

func Credential

func Credential(tok *oauth2.Token, clientID string) auth.Credential

Credential converts a token response into a stored credential.

func IsInvalidGrant

func IsInvalidGrant(err error) bool

IsInvalidGrant reports whether the server rejected a grant as invalid, expired or revoked.

func LockPath

func LockPath(dir, host string) string

LockPath returns the refresh lock file for host inside dir.

func LockSession

func LockSession(ctx context.Context, lockPath string) (func(), error)

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

func NewClient(apiURL string, httpClient *http.Client) *Client

NewClient returns a client for the API at apiURL. The client id defaults to DefaultClientID unless ClientIDEnv is set.

func (*Client) Endpoints

func (c *Client) Endpoints(ctx context.Context) Endpoints

Endpoints discovers the server metadata once (RFC 8414) and falls back to the conventional Nimbu URLs when discovery is unavailable.

func (*Client) LoginWithBrowser

func (c *Client) LoginWithBrowser(ctx context.Context, opts BrowserLogin) (*oauth2.Token, error)

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

func (c *Client) LoginWithDevice(ctx context.Context, opts DeviceLogin) (*oauth2.Token, error)

LoginWithDevice requests a user code, shows it through opts.Prompt and polls the token endpoint until the user approves, denies or the code expires.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, refreshToken string) (*oauth2.Token, error)

Refresh redeems a refresh token. The server rotates it: the old token is dead once this returns, so callers must persist the result.

func (*Client) Revoke

func (c *Client) Revoke(ctx context.Context, token, hint string) error

Revoke revokes a token (RFC 7009). The server answers 200 for unknown tokens too, so an error only means the request itself failed.

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

func FallbackEndpoints(apiURL string) Endpoints

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.

func (*TokenSource) Renew

func (s *TokenSource) Renew(ctx context.Context, rejected string) (string, error)

Renew replaces an access token the API rejected. Concurrent callers that saw the same rejected token share one refresh.

func (*TokenSource) Token

func (s *TokenSource) Token(ctx context.Context) (string, error)

Token returns a usable access token, refreshing it when it is about to expire.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL