authstore

package
v8.78.1 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package authstore holds the identities devctl's agent-facing commands act with: a GitHub user access token of the devctl GitHub App, obtained with the device flow and refreshed without a human, and a CircleCI API token obtained with the OAuth 2.0 authorization code flow with PKCE after a one-time dynamic client registration per device. Both live in the OS keychain with their expiry and nowhere else: no environment variable, no file, no output.

A command that needs a token calls RequireGitHub or RequireCircleCI before it does anything else and returns the ErrAuthRequired it gets unchanged: it is exit 8 with one sentence naming `devctl auth login`.

Index

Constants

View Source
const (
	Service      = "devctl"
	UserGitHub   = "github"
	UserCircleCI = "circleci"
)

The keychain coordinates: one service, one record per identity.

View Source
const CircleCIExpiryWarning = 7 * 24 * time.Hour

CircleCIExpiryWarning is how long before a CircleCI token expires the commands warn about it; the token has no refresh, a human re-authorizes.

View Source
const GitHubAppClientID = "PLACEHOLDER-CLIENT-ID"

GitHubAppClientID is the client id of the devctl GitHub App. The device flow and the refresh need nothing else: an App used through the device flow refreshes without a client secret.

Variables

View Source
var ErrAuthRequired = errors.New("authentication required")

ErrAuthRequired is what every *AuthRequiredError matches with errors.Is.

View Source
var ErrNotFound = errors.New("no record in the keychain")

ErrNotFound is returned when the identity has no record.

Functions

func OpenBrowser

func OpenBrowser(url string) error

OpenBrowser opens url in the platform's default browser and returns once the opener has started; the page loads in the background.

Types

type Auth

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

Auth runs the flows and answers the token questions against one store.

func New

func New(config Config) (*Auth, error)

New returns an Auth for config.

func Open

func Open(stderr io.Writer) (*Auth, error)

Open is the Auth the environment configures: endpoints and store from agentcli.EndpointsFromEnv, the clock from agentcli.SystemClock. stderr nil means os.Stderr.

func (*Auth) LoginCircleCI

func (a *Auth) LoginCircleCI(ctx context.Context) (Identity, error)

LoginCircleCI runs the authorization code flow with PKCE: listens on the loopback address of the device's client (registering a client first when the device has none, or when its address cannot be bound any more), prints the authorization URL to stderr, opens the browser, exchanges the code, reads the login and stores the record. Re-authorizing the same client revokes the previous token on CircleCI's side.

func (*Auth) LoginGitHub

func (a *Auth) LoginGitHub(ctx context.Context) (Identity, error)

LoginGitHub runs the device flow: prints the verification URL and the user code to stderr, opens the browser, polls until the human has authorized, reads the login and stores the record. The token never leaves the store.

func (*Auth) RequireCircleCI

func (a *Auth) RequireCircleCI(_ context.Context) (Token, error)

RequireCircleCI is the CircleCI token while it is valid and ErrAuthRequired when missing or expired; there is no refresh. The token's Warning is set within CircleCIExpiryWarning of the expiry.

func (*Auth) RequireGitHub

func (a *Auth) RequireGitHub(ctx context.Context) (Token, error)

RequireGitHub is the GitHub token: the stored one while it is valid, a refreshed one when it expired and the refresh token has not, and ErrAuthRequired otherwise.

func (*Auth) Status

func (a *Auth) Status() (Status, error)

Status reads both records without touching the network.

type AuthRequiredError

type AuthRequiredError struct {
	// Identity is "GitHub" or "CircleCI".
	Identity string
	// Cause says what is wrong with the record, without token material.
	Cause string
	// Hint is the command that fixes it.
	Hint string
}

AuthRequiredError is exit 8: no usable token for Identity. Its message is the one sentence the envelope's reason carries.

func (*AuthRequiredError) Error

func (e *AuthRequiredError) Error() string

func (*AuthRequiredError) ExitCode

func (e *AuthRequiredError) ExitCode() int

ExitCode implements agentcli.ExitCoder.

func (*AuthRequiredError) ExitVerdict

func (e *AuthRequiredError) ExitVerdict() agentcli.Verdict

ExitVerdict implements agentcli.ExitCoder.

func (*AuthRequiredError) Is

func (e *AuthRequiredError) Is(target error) bool

Is makes errors.Is(err, ErrAuthRequired) true.

type Config

type Config struct {
	// Store is required.
	Store Store
	// Endpoints default to production.
	Endpoints agentcli.Endpoints
	// Clock defaults to the system clock at scale 1.
	Clock agentcli.Clock
	// HTTPClient defaults to one with a timeout.
	HTTPClient *http.Client
	// OpenBrowser opens a URL for the human; nil means the platform's opener.
	OpenBrowser func(url string) error
	// Stderr receives the instructions for the human during login (the
	// verification URL and code), never a token; nil means os.Stderr.
	Stderr io.Writer
}

Config configures an Auth.

type FileStore

type FileStore struct {
	Path string
}

FileStore keeps the records in one JSON file with mode 0600, for tests and machines without a keychain daemon.

func (*FileStore) Delete

func (s *FileStore) Delete(user string) error

Delete implements Store.

func (*FileStore) Get

func (s *FileStore) Get(user string) (Record, error)

Get implements Store.

func (*FileStore) Set

func (s *FileStore) Set(user string, record Record) error

Set implements Store.

type Identity

type Identity struct {
	// Present is false when the keychain has no record.
	Present bool   `json:"present"`
	Login   string `json:"login,omitempty"`
	// ExpiresAt is absent for a token that does not expire.
	ExpiresAt *time.Time `json:"expiresAt,omitempty"`
	Expired   bool       `json:"expired"`
	// Refreshable: the record has a refresh token that has not expired, so a
	// command refreshes the access token itself (GitHub only).
	Refreshable      bool       `json:"refreshable"`
	RefreshExpiresAt *time.Time `json:"refreshExpiresAt,omitempty"`
	Warnings         []string   `json:"warnings"`
}

Identity is what `auth status` says about one identity: never the token.

func (Identity) Usable

func (i Identity) Usable() bool

Usable: a command can act with this identity without a human.

type KeyringStore

type KeyringStore struct{}

KeyringStore is the OS keychain: Secret Service on Linux, Keychain on macOS, Credential Manager on Windows.

func (KeyringStore) Delete

func (KeyringStore) Delete(user string) error

Delete implements Store.

func (KeyringStore) Get

func (KeyringStore) Get(user string) (Record, error)

Get implements Store.

func (KeyringStore) Set

func (KeyringStore) Set(user string, record Record) error

Set implements Store.

type Record

type Record struct {
	// Login is the account the token acts as, read once at login.
	Login string `json:"login,omitempty"`
	// Token is the access token.
	Token     string    `json:"token"`
	ExpiresAt time.Time `json:"expiresAt"`
	// RefreshToken and RefreshExpiresAt: GitHub only.
	RefreshToken     string    `json:"refreshToken,omitempty"`
	RefreshExpiresAt time.Time `json:"refreshExpiresAt"`
	// ClientID and RedirectURI: CircleCI only, the per-device OAuth client
	// devctl registered and the loopback address it registered.
	ClientID    string `json:"clientId,omitempty"`
	RedirectURI string `json:"redirectUri,omitempty"`
}

Record is what the keychain holds for one identity. A zero time means the value does not expire.

type Status

type Status struct {
	GitHub   Identity `json:"github"`
	CircleCI Identity `json:"circleci"`
}

Status is both identities.

func NewStatus

func NewStatus() Status

NewStatus is the status before anything is known: both identities absent, their warnings empty arrays.

func (Status) Check

func (s Status) Check() error

Check is nil when both identities are usable, else the *AuthRequiredError of the first one that is not, hinting at the login that fixes it.

type Store

type Store interface {
	// Get returns the record of user or ErrNotFound.
	Get(user string) (Record, error)
	Set(user string, record Record) error
	// Delete removes the record; a missing record is not an error.
	Delete(user string) error
}

Store keeps the records. Implementations: the OS keychain and, for tests, a file.

func OpenStore

func OpenStore(endpoints agentcli.Endpoints) Store

OpenStore is the store the environment selects: the file of agentcli.EnvKeyringFile when set, the OS keychain otherwise.

type Token

type Token struct {
	// Value is the token. It goes into a request header and nowhere else.
	Value string
	// Login is the account the token acts as.
	Login string
	// ExpiresAt is zero for a token that does not expire.
	ExpiresAt time.Time
	// Warning is the expiry notice of a CircleCI token within
	// [CircleCIExpiryWarning] of its end, for the envelope; empty otherwise.
	Warning string
}

Token is a usable access token with what a command may say about it.

func RequireCircleCI

func RequireCircleCI(ctx context.Context) (Token, error)

RequireCircleCI is the CircleCI token of the environment's store; ErrAuthRequired when missing or expired. Token.Warning carries the seven-day expiry notice.

func RequireGitHub

func RequireGitHub(ctx context.Context) (Token, error)

RequireGitHub is the GitHub token of the environment's store, refreshed when expired and refreshable; ErrAuthRequired otherwise.

Jump to

Keyboard shortcuts

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