Documentation
¶
Overview ¶
Package auth finds the GitHub token, runs the GitHub App device flow, and keeps the login in the OS keychain. A token is never printed, logged, written to a file or put in an error message.
Index ¶
- Constants
- Variables
- func DeleteLogin(s keychain.Store) error
- func FileLock(path string, now func() time.Time, wait time.Duration) func() (func(), error)
- func GHCLI() (string, error)
- func Kind(token string) string
- func LooksLikeAgent(getenv func(string) string, consoleUsable bool) bool
- func NoGH(getenv func(string) string) bool
- func Resolve(ctx context.Context, src Source) (string, string, error)
- func SaveLogin(s keychain.Store, l Login) error
- type DeviceCode
- type DeviceFlow
- type Login
- type Source
Constants ¶
const ( Service = "ghostbranch" Account = "github.com" )
Service and Account name the keychain entry that holds the login.
const ( // RefreshWindow: a stored app token this close to expiry is refreshed // before it is used. RefreshWindow = 5 * time.Minute // LockWait is how long a command waits for another one's refresh. LockWait = 30 * time.Second )
const ( BotName = "ghostbranch-cli[bot]" BotEmail = "340449680+ghostbranch-cli[bot]@users.noreply.github.com" )
BotName and BotEmail sign the commits that only ever live in the shadow repo. 340449680 is the app bot user's GitHub ID (GET /users/ghostbranch-cli[bot]); GitHub links <id>+<login>@users.noreply.github.com to that account and shows the app's name and logo. Both change if the app is renamed or recreated, so they live next to ClientID and AppSlug.
Variables ¶
var ( ClientID = "Iv23liqDTESZ1JStXO0g" AppSlug = "ghostbranch-cli" )
ClientID is the Ghostbranch GitHub App's client ID, which is public by design (there is no client secret). AppSlug is the app's URL name, for its install page. Both are empty in builds made before the app was registered; login then offers only --with-token.
Functions ¶
func DeleteLogin ¶
DeleteLogin removes the stored login, a corrupt one included; nothing stored is not an error. A newer Ghostbranch's login is kept.
func FileLock ¶
FileLock is a Source.Lock over the OS file lock at path (workspace.Lock, which frees itself when its process dies). It waits up to wait for another command's hold to end, then gives that command's LOCKED.
func LooksLikeAgent ¶
LooksLikeAgent reports whether this run is likely an agent or a script rather than a person at a terminal: GHOSTBRANCH_ACTOR names an agent or the app, a coding agent's marker variable is set, or there is no usable console. GHOSTBRANCH_ACTOR=human:… says it is a person.
func NoGH ¶ added in v0.2.0
NoGH reports whether GHOSTBRANCH_NO_GH turns off the `gh auth token` fallback: 1, true or yes, in any case, after trimming spaces.
func Resolve ¶
Resolve returns the token and where it came from, in this order: GHOSTBRANCH_TOKEN, the stored login ("login"), GH_TOKEN, GITHUB_TOKEN, `gh auth token` (off when GHOSTBRANCH_NO_GH is set; see NoGH). With GHOSTBRANCH_TOKEN set the keychain is never touched. A stored login that can't be used (corrupt, locked keychain, expired, newer) is an error, never a quiet fall back to a later source; only a machine with no keychain at all goes on.
Types ¶
type DeviceCode ¶
func (DeviceCode) GoString ¶
func (d DeviceCode) GoString() string
func (DeviceCode) String ¶
func (d DeviceCode) String() string
String keeps the device code, a live secret until it expires, out of any %v, %+v, %#v or %s. The user code and URL are shown to the person anyway.
type DeviceFlow ¶
type DeviceFlow struct {
ClientID string
BaseURL string // "https://github.com"; tests point it at authtest
HTTP *http.Client
Sleep func(time.Duration)
Now func() time.Time
}
DeviceFlow is GitHub's OAuth device flow for the Ghostbranch GitHub App: POST /login/device/code, then poll POST /login/oauth/access_token. Its refresh needs no client secret (device-flow tokens), so there is no server.
func (DeviceFlow) Poll ¶
func (d DeviceFlow) Poll(ctx context.Context, dc DeviceCode) (Login, error)
Poll waits for the person to approve the code. It sleeps the interval before each poll (at least 5 s), adds 5 s on slow_down (or takes GitHub's larger interval), stops when the code's life is over, and returns ctx's error on cancel. A network blip while waiting is retried. Account is left empty: the caller fills it from the API.
func (DeviceFlow) Refresh ¶
Refresh trades a refresh token for a new pair. GitHub invalidates the old pair, so callers refresh under Source.Lock. A refused refresh token is LOGIN_EXPIRED.
func (DeviceFlow) Start ¶
func (d DeviceFlow) Start(ctx context.Context) (DeviceCode, error)
Start asks GitHub for a device code and the code the person types.
type Login ¶
type Login struct {
Token, RefreshToken string
Expires, RefreshExpires time.Time
Account string // GitHub login
Kind string // "app" (device flow) | "pat" (pasted)
SchemaVersion string
}
Login is the stored GitHub login. Expiries are zero for a pasted token.
func LoadLogin ¶
LoadLogin reads the stored login: (nil, nil) when none is stored.
- A keychain failure is LOGIN_UNAVAILABLE (no_keychain or keychain_error) wrapping the keychain error, so errors.Is(err, keychain.ErrUnavailable) works.
- An entry from a newer Ghostbranch is UPGRADE_NEEDED.
- An unreadable entry is LOGIN_UNAVAILABLE (corrupt).
No message quotes the entry or a decoder error, which could carry its text.
func (Login) MarshalJSON ¶
MarshalJSON refuses: a Login reaches storage only through SaveLogin, and never output.
type Source ¶
type Source struct {
Getenv func(string) string
GH func() (string, error)
Store keychain.Store // nil: no stored login is consulted
Refresh func(ctx context.Context, l Login) (Login, error)
Lock func() (unlock func(), err error) // <GHOSTBRANCH_HOME>/locks/auth.lock in the CLI
Now func() time.Time
// OnSaveError hears that a refreshed login couldn't be stored. The
// error may carry a secret, so it is for tests and logic, never output.
OnSaveError func(error)
}
Source is everything Resolve reads.