oauth

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 20 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrNoTokens = errors.New("no stored tokens for profile")

ErrNoTokens is returned when no tokens are stored for the given profile.

Functions

func AccessTokenForProfile

func AccessTokenForProfile(ctx context.Context, profile *config.Profile) (string, error)

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

func Challenge(verifier string) string

Challenge returns the S256 code challenge for the given verifier: base64url(sha256(verifier)) with no padding.

func DeleteTokens

func DeleteTokens(profile string) error

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

func GenerateVerifier() (string, error)

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

func IsUnauthorizedError(err error) bool

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

func NewState() (string, error)

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

func Open(url string) error

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

func StartLoopbackServer(ctx context.Context) (int, <-chan Callback, func(), error)

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

type Callback struct {
	Code  string
	State string
	Error string
}

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

type OAuthError struct {
	Code        string
	Description string
	Status      int
}

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.

type TokenUser

type TokenUser struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name,omitempty"`
	Image string `json:"image,omitempty"`
}

TokenUser is the nested user object returned by the backend token endpoints.

Jump to

Keyboard shortcuts

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