auth

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package auth implements login against Auth0 using the OAuth device authorization grant, refresh-token storage in the OS keychain, and a per-session TokenSource that refreshes transparently. Endpoints come from build-time variables so no tenant details ship in source.

Index

Constants

View Source
const ServiceName = "sweetrpg-cli"

ServiceName is the OS-keychain service all CLI credentials live under.

Variables

View Source
var (
	Domain   string // e.g. "dev-abc123.us.auth0.com"
	ClientID string
	Audience string // catalog-api identifier
	Scopes   = "openid profile email offline_access"
)

Build-time settings. Empty means auth is not configured; commands that need it fail with a clear message instead of guessing a tenant.

View Source
var (
	ErrDenied  = errors.New("authorization denied")
	ErrExpired = errors.New("device code expired before authorization completed")
	ErrPending = errors.New("authorization pending")
)

ErrDenied and ErrExpired end the flow; every other error is transport or server trouble surfaced to the caller as-is.

View Source
var ErrNotLoggedIn = errors.New("not logged in: run 'sweetrpg auth login'")

ErrNotLoggedIn means no usable stored credentials exist. Callers map it to exit code 3 with a pointer at `auth login`.

Functions

func IsAuthRequired

func IsAuthRequired(err error) bool

IsAuthRequired reports whether err should exit 3 with a login hint.

func Logout

func Logout(ctx context.Context, hc *http.Client, cfg *Config, st Store) (bool, error)

Logout revokes the stored refresh token server-side (best effort) and deletes local credentials. Missing credentials are not an error, so logout is idempotent; the bool reports whether credentials existed.

func RevokeRefreshToken

func RevokeRefreshToken(ctx context.Context, hc *http.Client, cfg *Config, refreshToken string) error

RevokeRefreshToken asks Auth0 to invalidate the refresh token. Failures are returned but logout treats them as non-fatal.

func SleepContext

func SleepContext(ctx context.Context, d time.Duration) error

SleepContext is the production sleeper: waits d or until ctx is done.

Types

type AccessToken

type AccessToken struct {
	Token     string
	ExpiresAt time.Time
}

AccessToken pairs a bearer token with its expiry for session caching.

func (AccessToken) Valid

func (a AccessToken) Valid(now time.Time) bool

Valid reports whether the token can still be used with margin to spare so requests never race the expiry clock mid-flight.

type AuthError

type AuthError struct{ Msg string }

AuthError marks failures that need `auth login` again. The command layer maps them to exit code 3.

func (*AuthError) Error

func (e *AuthError) Error() string

type Claims

type Claims struct {
	Subject string `json:"sub"`
	Email   string `json:"email"`
}

Claims are the ID-token fields the CLI cares about. The token arrives directly from Auth0 over TLS in this process, so fields are read without signature verification - verification is Auth0's job here.

func Login

func Login(ctx context.Context, hc *http.Client, cfg *Config, st Store, sleep func(context.Context, time.Duration) error, openURL func(string)) (*Claims, error)

Login performs the full device flow and persists the resulting session. sleep is injectable for tests; production passes SleepContext. openURL, when non-nil, is tried with the verification URL as a convenience.

func ParseIDTokenClaims

func ParseIDTokenClaims(idToken string) (*Claims, error)

ParseIDTokenClaims decodes the payload segment of a JWT. It returns an error for malformed tokens rather than guessing.

type Config

type Config struct {
	Domain   string
	ClientID string
	Audience string
	Scopes   string

	// PollIntervalFloor guards against servers reporting interval 0; RFC 8628
	// suggests treating anything under 5s as 5s.
	PollIntervalFloor time.Duration
}

Config is one resolved tenant + client pairing. Tests build it directly; production resolves it from the vars above via DefaultConfig.

func DefaultConfig

func DefaultConfig() (*Config, error)

DefaultConfig resolves auth settings from the build-time values only (no env var or config file override). Most callers want ResolveConfig instead; this exists for callers with no config file context.

func ResolveConfig

func ResolveConfig(fileDomain, fileClientID, fileAudience string) (*Config, error)

ResolveConfig resolves the Auth0 tenant by precedence: env var > fileDomain/fileClientID/fileAudience (the config file's authTenant section) > the build-time Domain/ClientID/Audience vars baked in via -ldflags at release time. A release binary ships with the tenant baked in, but an operator can still repoint it via env var or config file without a rebuild - the baked value is a default, not a hardcode. It errors when no source provides a full configuration.

type DeviceCode

type DeviceCode 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"` // seconds
	Interval                int    `json:"interval"`   // seconds
	// contains filtered or unexported fields
}

DeviceCode is the start of one authorization attempt.

type KeyringStore

type KeyringStore struct{}

KeyringStore persists sessions in the OS keychain via go-keyring.

func (KeyringStore) Delete

func (KeyringStore) Delete() error

func (KeyringStore) Load

func (KeyringStore) Load() (*Session, error)

func (KeyringStore) Save

func (KeyringStore) Save(s Session) error

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-process Store for tests and for holding a session that could not be persisted (the refuse-to-persist fallback keeps it in memory for the rest of the run only).

func NewMemoryStore

func NewMemoryStore() *MemoryStore

func (*MemoryStore) Delete

func (m *MemoryStore) Delete() error

func (*MemoryStore) Load

func (m *MemoryStore) Load() (*Session, error)

func (*MemoryStore) Save

func (m *MemoryStore) Save(s Session) error

type PollStatus

type PollStatus int

PollStatus classifies one token-endpoint poll result.

const (
	StatusAuthorized PollStatus = iota
	StatusPending               // user has not approved yet - keep polling
	StatusSlowDown              // polling too fast - back off and keep polling
)

type Session

type Session struct {
	Account      string `json:"account"`
	Email        string `json:"email,omitempty"`
	RefreshToken string `json:"refresh_token"`
}

Session is what persists between runs: who logged in and the refresh token that proves it. Access tokens are never persisted - they are short-lived and re-derived from the refresh token.

type SessionSource

type SessionSource struct {
	Cfg   *Config
	HTTP  *http.Client
	Store Store

	// RotateSaveErr records a failed save after refresh-token rotation: the
	// current run keeps working, but the next run will need to log in again.
	RotateSaveErr error
	// contains filtered or unexported fields
}

SessionSource supplies bearer tokens for one logged-in session, refreshing transparently and persisting rotations.

func (*SessionSource) Token

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

Token returns a valid bearer token, refreshing on expiry or first use.

type Store

type Store interface {
	Save(Session) error
	Load() (*Session, error)
	Delete() error
}

Store abstracts credential persistence so commands stay testable and so a broken keychain degrades to session-only use instead of plaintext files.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	IDToken      string `json:"id_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	Scope        string `json:"scope"`
}

TokenResponse is a successful token endpoint reply. RefreshToken may be empty on refresh grants that do not rotate it.

func AwaitAuthorization

func AwaitAuthorization(ctx context.Context, hc *http.Client, cfg *Config, dc *DeviceCode, sleep func(context.Context, time.Duration) error) (*TokenResponse, error)

AwaitAuthorization runs the polling loop until the user approves, denies, the code expires, or ctx is cancelled. sleep is called between polls with the delay to observe; production passes a context-aware sleeper.

func RefreshAccessToken

func RefreshAccessToken(ctx context.Context, hc *http.Client, cfg *Config, refreshToken string) (*TokenResponse, error)

RefreshAccessToken exchanges a refresh token for a fresh token set. When Auth0 rotates the refresh token the new one is in RefreshToken; otherwise that field is empty and the caller keeps the old one.

Jump to

Keyboard shortcuts

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