githubauth

package
v0.1.0-rc.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 18 Imported by: 0

README

GitHub device authentication

This package owns the daemon's GitHub.com connection, local authentication API and token resolution. The desktop sign-in UI is implemented separately. Manager owns one active device authorization and one selected account. Its public Device and Status values exclude tokens and the device secret. Credential and Token are only for trusted daemon code, never API responses, logs or model environments.

The public OAuth client ID is Ov23liFSMjrhof9WQlOi. Device flow requests repo for private repositories and read:org for team review requests. The browser uses GitHub's fixed device verification page. HTTP exchanges have a 15-second limit, a 64 KiB response limit, no redirects, and no upstream error text. Poll performs one exchange, enforces the server interval, and adds five seconds on slow_down. Callers should poll with a request context and stop at terminal states. Starting again replaces the previous pending authorization.

The OAuth App has token expiration turned off, so a grant has no expiry and no refresh token, and the credential lasts until the user revokes it. GitHub refreshes an OAuth App's expiring tokens only with the client secret, which a desktop binary cannot keep, so expiring grants from before that change are refreshed with the public client ID and refresh token, without a client secret, and any refusal asks the user to reconnect. Refresh is serialized with account changes. /user checks account identity and OAuth scopes before persistence. Rotated credentials must be saved before the new access token is returned. An unavailable store fails closed, with no plaintext fallback. Disconnect deletes the local credential; it does not revoke authorization on GitHub.

ProcessStore uses macOS Keychain, Windows Credential Manager, or Linux Secret Service through the pure-Go go-keyring package. The host is the service namespace (baum.github.com); the selected account and tokens occupy one atomic item (desktop-account), so replacing accounts leaves no previous account item. Enterprise device authorization and multiple simultaneous accounts are outside this first version. Native keyring operations run in the hidden baum internal github-credential helper with a 20-second deadline. Before reading a request or opening the keyring, the helper verifies its parent's executable identity using operating system process metadata and requires pipe input and output. On Linux the parent starts the helper from /proc/self/exe and the helper compares its own /proc/self/exe with the parent's, so a daemon whose file an update replaced keeps its credential access until it restarts into the new one. A shell or coding agent invoking the hidden command directly is refused, including save and delete requests. Credentials pass through private pipes. This trusts the installed baum executable and the OS process metadata; unsupported operating systems fail closed. It does not isolate credentials from arbitrary native code that can already access the user's keyring or modify the installed binary. Cancellation kills the helper process group (including macOS security), waits for process termination, then releases the manager lock. Lock acquisition is also cancellable. Native keyring code is private to the helper. No detached saves can race disconnect. Rotated grants remain in daemon memory until validation and persistence succeed; retries never reuse the spent refresh token. Lifetimes derive from the original token receipt time. If a retained access token nears expiry during an outage, its latest refresh token obtains a new pair before validation and persistence resume. A daemon crash before successful persistence can require reconnecting; credentials are never cached on disk.

Run just oss-go test -race ./internal/githubauth from the repository root.

The daemon shares one manager with configuration, GitHub HTTP clients and the local authentication controller. Explicit environment/config credentials take precedence. No GitHub CLI executable, token store or host registry is consulted. Enterprise credentials require an explicit matching GH_HOST; device sign-in is GitHub.com only. Cached GraphQL clients resolve credentials on every request; a discovery reader aborts if its credential partition changes mid-transaction.

The local /api/github-auth namespace uses the existing Host, session, Origin and CSRF boundary. GET observes safe status (state, host, login, source, override, saved, optional public pending, error, message). POST /start, /cancel, /disconnect and /retry mutate the connection. Go owns token polling; the browser never receives access/refresh tokens or device secrets. /retry rechecks a recovered credential store without starting a new authorization. Error codes include credential_store_unavailable, auth_request_failed, reconnect_required, insufficient_scopes, sso_required and permission_denied. Terminal device states include denied, expired and cancelled.

Automatic scheduling is fenced while device sign-in is pending and resumes on cancel/error expiry. An effective account change establishes fresh PR baselines without generating historical work. Existing preferences and saved reviews remain. In-flight discovery is cancelled and obsolete snapshots/observations cannot enter the new account's partition. Final review publication using the saved GitHub.com account is serialized against account replacement. Reviews using explicit environment/config or Enterprise credentials keep running and may publish when the unrelated saved account changes. Persistence attempts are conservatively treated as identity changes even on failure: a native helper may commit a write before its reply is lost. A failed write may require retrying the store or reconnecting; baum never assumes the durable state was unchanged.

HTTPS Git fetches use the selected host credential through Git's private child environment, scoped to the exact remote URL. Credential helpers are disabled for that fetch, redirects are refused, and neither argv nor disk configuration holds the token. Authenticated Git failure messages discard upstream stderr. SSH fetches retain the user's working configuration. Native credential-store and live GitHub OAuth smoke tests still require each supported OS and a real account.

The same token also lists the account's repositories for picking (github.Client.ViewerRepositories, GraphQL to the host's API endpoint, needing only the repo read:org sign-in) and clones a picked repository (gitlocal.Clone, HTTPS to github.com with the same private-environment header as a fetch). A listing failure maps to the connection error codes above plus not_connected and rate_limited, using the runtime health this package keeps. See internal/githubrepos.

Queued review confirmations include the credential partition inside the existing configuration digest. A job admitted just before switching accounts cannot start with the replacement account, including after daemon restart. Baum-issued credentials use the validated account ID for their private partition, so same-account refresh preserves queued confirmations. Explicit overrides use a token fingerprint and require a fresh start when replaced. A discovery reader still refuses an exact token change mid-transaction and retries with a new reader; a late old-token identity lookup cannot inherit a replacement account's ID.

Documentation

Overview

Package githubauth owns GitHub.com device authorization and credentials.

Index

Constants

View Source
const ClientID = "Ov23liFSMjrhof9WQlOi"
View Source
const Scopes = "repo read:org"

Variables

View Source
var ErrNotConnected = errors.New("GitHub is not connected")
View Source
var ErrPermissions = errors.New("check repository access and GitHub authorization permissions")
View Source
var ErrReconnect = errors.New("GitHub authorization expired or was revoked; reconnect GitHub")
View Source
var ErrRequest = errors.New("GitHub authentication request failed; try again")
View Source
var ErrSSO = errors.New("authorize baum for your organization's GitHub SSO and retry")
View Source
var ErrScopes = errors.New("GitHub authorization requires repo and read:org access")
View Source
var ErrStore = errors.New("GitHub credential store unavailable; unlock your operating system credential store and try again")

Functions

func ServeCredentialHelper

func ServeCredentialHelper(ctx context.Context, input io.Reader, output io.Writer) error

ServeCredentialHelper is only for the hidden CLI helper. The native store runs synchronously here; ProcessStore owns this process's deadline and waits for termination before releasing its caller's serialization lock.

Types

type Credential

type Credential struct {
	AccessToken      string `json:"access_token"`
	RefreshToken     string `json:"refresh_token,omitempty"`
	ExpiresAt        int64  `json:"expires_at,omitempty"`
	RefreshExpiresAt int64  `json:"refresh_expires_at,omitempty"`
	Login            string `json:"login"`
	ID               int64  `json:"id"`
}

Credential is daemon-only state and must never be exposed by a public API. JSON is reserved for persistence in the operating system credential store.

func (Credential) GoString

func (Credential) GoString() string

func (Credential) String

func (Credential) String() string

String and GoString keep routine diagnostics from disclosing credentials.

type Device

type Device struct {
	ID              string    `json:"id"`
	UserCode        string    `json:"user_code"`
	VerificationURI string    `json:"verification_uri"`
	ExpiresAt       time.Time `json:"expires_at"`
	Interval        int       `json:"interval"`
}

type Manager

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

Manager serializes credential refresh, persistence and account replacement. Poll performs at most one exchange; callers can poll freely without exceeding GitHub's requested interval. Only one device authorization is active at a time.

func New

func New(store Store) *Manager

func NewWithOptions

func NewWithOptions(store Store, opts Options) *Manager

func (*Manager) AccountForToken

func (m *Manager) AccountForToken(token string) int64

AccountForToken identifies only the credential most recently issued by Token. A late lookup for an older token never inherits the replacement account ID.

func (*Manager) Cancel

func (m *Manager) Cancel(ctx context.Context, id string) error

func (*Manager) Disconnect

func (m *Manager) Disconnect(ctx context.Context) error

func (*Manager) Health

func (m *Manager) Health() error

func (*Manager) IdentityContext

func (m *Manager) IdentityContext(ctx context.Context) (context.Context, func())

IdentityContext cancels evidence work when the selected account changes. Token refresh does not cancel work because it retains the same identity.

func (*Manager) ObserveResponse

func (m *Manager) ObserveResponse(token string, status int, headers http.Header)

ObserveResponse ignores late failures for a credential no longer in use. Upstream bodies and header values are never retained or exposed.

func (*Manager) Poll

func (m *Manager) Poll(ctx context.Context, id string) (Status, error)

func (*Manager) Revision

func (m *Manager) Revision() uint64

func (*Manager) Start

func (m *Manager) Start(ctx context.Context) (Device, error)

func (*Manager) Status

func (m *Manager) Status(ctx context.Context) (Status, error)

func (*Manager) Token

func (m *Manager) Token(ctx context.Context) (token string, returnedErr error)

func (*Manager) WithIdentity

func (m *Manager) WithIdentity(ctx context.Context, publish func() error) error

WithIdentity serializes final local publication with account replacement. No network or model work may run inside publish.

type Options

type Options struct{ Transport http.RoundTripper }

type ProcessStore

type ProcessStore struct{ Executable string }

ProcessStore bounds native credential-store prompts by running them in a helper process. Secrets travel only through private pipes, never argv or env. Every operation waits for the helper to exit before returning, so cancellation cannot leave a detached save racing a later disconnect.

func (ProcessStore) Delete

func (s ProcessStore) Delete(ctx context.Context) error

func (ProcessStore) Load

func (s ProcessStore) Load(ctx context.Context) (Credential, error)

func (ProcessStore) Save

func (s ProcessStore) Save(ctx context.Context, c Credential) error

type Status

type Status struct {
	State     string `json:"state"`
	Login     string `json:"login,omitempty"`
	AccountID int64  `json:"account_id,omitempty"`
}

type Store

type Store interface {
	Load(context.Context) (Credential, error)
	Save(context.Context, Credential) error
	Delete(context.Context) error
}

Jump to

Keyboard shortcuts

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