Documentation
¶
Overview ¶
Package oauth implements the OAuth 2.0 flows used by the PromptVM CLI: the Authorization Code flow with PKCE over a loopback redirect (RFC 8252) and the Device Authorization Grant (RFC 8628). It also provides secure token storage backed by the OS keychain with an encrypted-file fallback.
Index ¶
- Variables
- func AccessTokenForProfile(ctx context.Context, profile *config.Profile) (string, error)
- func Challenge(verifier string) string
- func DeleteTokens(profile string) error
- func GenerateVerifier() (string, error)
- func IsUnauthorizedError(err error) bool
- func NewState() (string, error)
- func Open(url string) error
- func SaveTokens(profile string, tokens *StoredTokens) error
- func StartLoopbackServer(ctx context.Context) (int, <-chan Callback, func(), error)
- func WithAutoRefresh[T any](ctx context.Context, profile *config.Profile, fn func() (T, error)) (T, error)
- type Callback
- type DeviceCodeResponse
- type OAuthError
- type StoredTokens
- type TokenOrganization
- type TokenResponse
- func ExchangeCode(ctx context.Context, baseURL, code, verifier, redirectURI string) (*TokenResponse, error)
- func PollDeviceToken(ctx context.Context, baseURL, deviceCode string, interval int) (*TokenResponse, error)
- func RefreshToken(ctx context.Context, baseURL, refreshToken string) (*TokenResponse, error)
- type TokenUser
Constants ¶
This section is empty.
Variables ¶
var ErrNoTokens = errors.New("no stored tokens for profile")
ErrNoTokens is returned when no tokens are stored for the given profile.
Functions ¶
func AccessTokenForProfile ¶
AccessTokenForProfile returns a usable access token for the given profile, transparently refreshing it if it has expired (or is within refreshSkew of expiry).
For legacy api_key profiles it simply returns the stored API key. For OAuth profiles it loads tokens from the keychain, refreshes when needed, persists any new tokens, and returns the access token.
func Challenge ¶
Challenge returns the S256 code challenge for the given verifier: base64url(sha256(verifier)) with no padding.
func DeleteTokens ¶
DeleteTokens removes any stored tokens for the profile. It does not error if tokens were never stored. File fallback items are also removed.
func GenerateVerifier ¶
GenerateVerifier returns a cryptographically random PKCE code verifier encoded as an unpadded base64url string. RFC 7636 requires the verifier to be 43-128 characters; 32 random bytes → 43 characters.
func IsUnauthorizedError ¶
IsUnauthorizedError reports whether err looks like an OAuth 401 / expired-token response from the API. Checks both the literal string "401", "invalid_token", and "token_expired" markers.
func NewState ¶
NewState returns a cryptographically random state value, encoded as an unpadded base64url string. The state is compared against the value returned from the authorization server to protect against CSRF.
func Open ¶
Open opens the user's default browser pointed at url. Returns the underlying error if the browser could not be launched. Callers should treat failure as non-fatal — the URL should also be printed to stderr so the user can paste it manually.
func SaveTokens ¶
func SaveTokens(profile string, tokens *StoredTokens) error
SaveTokens persists both access and refresh tokens for the given profile. On systems without a usable keychain, it falls back to the encrypted file store in keychain_file.go.
func StartLoopbackServer ¶
StartLoopbackServer binds an HTTP server on 127.0.0.1 at a random port and returns the port, a channel that will receive exactly one Callback, and a shutdown func the caller must invoke when finished.
The server only listens on 127.0.0.1 (not 0.0.0.0 / localhost) per the loopback interface redirection guidance in RFC 8252.
Only /callback is handled; all other paths return 404.
func WithAutoRefresh ¶
func WithAutoRefresh[T any](ctx context.Context, profile *config.Profile, fn func() (T, error)) (T, error)
WithAutoRefresh executes fn. If fn fails with a 401-shaped error that mentions an expired token, WithAutoRefresh forces a refresh of the profile's OAuth tokens and retries fn exactly once.
The "shape" of an expired-token error is detected heuristically via IsUnauthorizedError — the SDK does not expose a typed 401 to us, but it does surface the status code and the server's error body in the error string.
Types ¶
type Callback ¶
Callback represents the data delivered to the loopback redirect URI. Exactly one of (Code, State) or Error will be populated.
type DeviceCodeResponse ¶
type DeviceCodeResponse struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"`
Interval int `json:"interval"`
}
DeviceCodeResponse is the RFC 8628 §3.2 device authorization response.
func RequestDeviceCode ¶
func RequestDeviceCode(ctx context.Context, baseURL, deviceName string) (*DeviceCodeResponse, error)
RequestDeviceCode starts a device authorization grant. deviceName is sent to the server as a human label so the user can distinguish this session in the authorized-devices list later.
type OAuthError ¶
OAuthError carries a structured OAuth error code and description so callers can branch on things like authorization_pending or slow_down.
func (*OAuthError) Error ¶
func (e *OAuthError) Error() string
type StoredTokens ¶
type StoredTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
StoredTokens is the subset of TokenResponse we persist. It excludes scope and user metadata since those live in the YAML profile.
func LoadTokens ¶
func LoadTokens(profile string) (*StoredTokens, error)
LoadTokens returns the stored tokens for a profile. Falls back to the file store if the keychain is unavailable.
type TokenOrganization ¶
type TokenOrganization struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
}
TokenOrganization is the nested organization object returned by the backend.
type TokenResponse ¶
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
Scope string `json:"scope,omitempty"`
User *TokenUser `json:"user,omitempty"`
Organization *TokenOrganization `json:"organization,omitempty"`
ExpiresAt time.Time `json:"-"`
}
TokenResponse is the normalized shape returned by every token-granting endpoint in this package. Expiry is computed at parse time so callers never need to know about raw ExpiresIn.
func ExchangeCode ¶
func ExchangeCode(ctx context.Context, baseURL, code, verifier, redirectURI string) (*TokenResponse, error)
ExchangeCode trades an authorization code + PKCE verifier for a token response. The redirect URI must match the one used when opening the browser.
func PollDeviceToken ¶
func PollDeviceToken(ctx context.Context, baseURL, deviceCode string, interval int) (*TokenResponse, error)
PollDeviceToken polls the device-token endpoint until the user authorizes the device, the code expires, or the context is cancelled.
Follows RFC 8628 §3.5 error handling:
- authorization_pending: continue polling at the same interval
- slow_down: add 5 seconds to the interval and continue
- expired_token: return an error instructing the user to re-run login
- access_denied: return "authorization denied"
func RefreshToken ¶
func RefreshToken(ctx context.Context, baseURL, refreshToken string) (*TokenResponse, error)
RefreshToken exchanges a refresh token for a fresh access/refresh pair. Callers are responsible for persisting the result back into the keychain.