auth

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package auth turns a raw HTTP credential into a normalized Actor and decides what that actor may do. M1 implements classic personal access tokens, the OAuth device flow, and the resolution path that backs GET /user; later milestones add fine-grained grants, GitHub Apps, and the repository authorizer.

Index

Constants

View Source
const (
	PrefixClassicPAT  = "ghp_"        // personal access token (classic)
	PrefixFineGrained = "github_pat_" // fine-grained PAT
	PrefixOAuth       = "gho_"        // OAuth app user access token
	PrefixUserToSrv   = "ghu_"        // GitHub App user-to-server token
	PrefixInstall     = "ghs_"        // GitHub App installation token
	PrefixRefresh     = "ghr_"        // GitHub App refresh token
)

Token class prefixes. The third letter classes the credential, matching GitHub's published token formats so a token minted here is detectable by the same secret-scanning rules.

View Source
const GHCLIClientID = "178c6fc778ccc68e1d6a"

GHCLIClientID is the OAuth client_id hardcoded into the gh CLI. gh sends it to POST /login/device/code on every "gh auth login", so the server must know the app before any user can sign in through gh.

Variables

View Source
var (
	ErrUnknownClient      = errors.New("auth: unknown client_id")
	ErrDeviceFlowDisabled = errors.New("auth: device flow not enabled for this app")
)

Errors returned by the device-flow request step. The REST layer renders them as OAuth error bodies.

View Source
var ErrBadCredentials = errors.New("auth: bad credentials")

ErrBadCredentials is returned when a credential is present but invalid, expired, or revoked. It maps to 401 at the HTTP layer.

View Source
var ErrInstallationSuspended = errors.New("auth: installation is suspended")

ErrInstallationSuspended is returned when a caller tries to mint a token for a suspended installation.

View Source
var ErrInvalidClientSecret = errors.New("auth: invalid client secret")

ErrInvalidClientSecret is returned when a confidential client presents a missing or wrong client_secret at the token exchange.

View Source
var ErrInvalidCode = errors.New("auth: invalid authorization code")

ErrInvalidCode is returned when an authorization code cannot be exchanged: it is unknown, already used, expired, or the redirect_uri does not match.

View Source
var ErrInvalidRedirectURI = errors.New("auth: redirect_uri is not under the registered callback")

ErrInvalidRedirectURI is returned when a redirect_uri does not sit under the app's registered authorization callback.

View Source
var ErrPATNotFound = errors.New("auth: personal access token not found")

ErrPATNotFound reports a delete aimed at a token the user does not have.

Functions

func HashToken

func HashToken(token string) [32]byte

HashToken returns the sha256 the store indexes tokens by. Every caller hashes through this so they agree byte-for-byte.

func VerifyChecksum

func VerifyChecksum(token string) bool

VerifyChecksum validates a presented token's class prefix and CRC32 offline, so a malformed or mistyped token is rejected without a database hit.

func WithActor

func WithActor(ctx context.Context, a *Actor) context.Context

WithActor returns a context carrying a.

Types

type Actor

type Actor struct {
	Kind Kind

	UserID    int64 // resolved user pk; 0 for anonymous
	UserLogin string
	SiteAdmin bool
	TokenID   int64 // tokens.pk of the credential used; 0 for anonymous

	// App-bound fields; non-zero only for KindInstallation and KindAppJWT.
	AppID          int64
	InstallationID int64

	Scopes    Scopes
	ExpiresAt *time.Time

	// RateKey identifies the rate-limit bucket this actor is charged against.
	RateKey string
}

Actor is the normalized principal placed in the request context. A request that reached the auth middleware always carries at least the anonymous actor, so handlers never nil-check.

func ActorFrom

func ActorFrom(ctx context.Context) *Actor

ActorFrom never returns nil: a request without a stored actor is treated as anonymous.

func Anonymous

func Anonymous() *Actor

Anonymous returns the unauthenticated actor.

func (*Actor) IsAuthenticated

func (a *Actor) IsAuthenticated() bool

IsAuthenticated reports whether a real credential resolved.

func (*Actor) IsUser

func (a *Actor) IsUser() bool

IsUser reports whether the actor acts as a user.

type CodeRequest added in v0.1.3

type CodeRequest struct {
	ClientID    string
	RedirectURI string
	Scope       string
	UserPK      int64
}

CodeRequest holds the parameters the caller passes to GenerateAuthCode.

type DeviceCodeResult

type DeviceCodeResult struct {
	DeviceCode      string
	UserCode        string
	VerificationURI string
	ExpiresIn       int
	Interval        int
}

DeviceCodeResult is the body of a successful POST /login/device/code.

type DeviceTokenOutcome

type DeviceTokenOutcome struct {
	Token            *IssuedToken
	Error            string // authorization_pending | slow_down | expired_token | access_denied | ...
	ErrorDescription string
	Interval         int // set with slow_down
}

DeviceTokenOutcome is the result of one poll of the device token endpoint. GitHub answers every poll with HTTP 200 and a JSON body that is either the token or an OAuth error, so the protocol-level conditions live here rather than in the returned error, which is reserved for genuine server failures.

type Generated

type Generated struct {
	Plaintext string   // shown once, never stored
	Prefix    string   // "ghp_" etc.
	Hash      [32]byte // sha256(Plaintext); store Hash[:] in tokens.token_hash
	Last8     string   // last eight chars, for the settings UI
}

Generated holds everything the store needs plus the one-time plaintext. The plaintext is shown to the user exactly once and never persisted.

func GenerateToken

func GenerateToken(prefix string) (Generated, error)

GenerateToken mints a token with the given class prefix. It never touches the database; the caller persists the hash.

type IssuedToken

type IssuedToken struct {
	AccessToken string
	TokenType   string
	Scope       string
}

IssuedToken is a minted user token returned by a completed device exchange.

type Kind

type Kind uint8

Kind classifies the credential behind an Actor.

const (
	KindAnonymous    Kind = iota // no or invalid credential, public-only access
	KindUser                     // classic PAT or OAuth user token
	KindUserToServer             // ghu_: a user bounded by an installation
	KindInstallation             // ghs_: an installation, no user
	KindAppJWT                   // app-level JWT
)

The actor kinds. M1 produces Anonymous and User; the App-related kinds are reserved for later milestones.

type PATInfo added in v0.1.3

type PATInfo struct {
	ID         int64
	Note       string
	Scopes     string // header form, e.g. "gist, repo"
	LastEight  string
	CreatedAt  time.Time
	LastUsedAt *time.Time
}

PATInfo is the displayable summary of a personal access token: everything the settings page shows, nothing that authenticates.

type Scope

type Scope string

Scope is a classic OAuth/PAT scope string, e.g. "repo" or "read:org".

type Scopes

type Scopes []Scope

Scopes is a set of classic scopes, kept sorted and deduplicated after NormalizeScopes so the X-OAuth-Scopes header round-trips byte-for-byte with GitHub for the common cases.

func NormalizeScopes

func NormalizeScopes(in Scopes) Scopes

NormalizeScopes keeps only known scopes, drops any implied by a held parent, then dedupes and sorts.

func ParseScopeParam

func ParseScopeParam(s string) Scopes

ParseScopeParam splits a space- or comma-delimited scope parameter (the OAuth "scope" field accepts both) into a Scopes set.

func (Scopes) Has

func (ss Scopes) Has(sc Scope) bool

Has reports whether the set effectively carries sc, expanding parents.

func (Scopes) Header

func (ss Scopes) Header() string

Header renders the set as GitHub's comma-space separated list, e.g. "gist, repo".

func (Scopes) Strings

func (ss Scopes) Strings() []string

Strings returns the scopes as a plain string slice, preserving order.

type Service

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

Service resolves credentials into Actors and serves the OAuth device flow. It is framework-agnostic: it speaks contexts, strings, and typed results, never http.ResponseWriter, so the REST layer owns all request and response wiring. The dependency direction is auth -> store only.

func NewService

func NewService(st Store, baseURL string) *Service

NewService wires a Service over the store. baseURL is the site root used in device-flow responses (for example https://git.example.com).

func (*Service) AppByPK added in v0.1.3

func (s *Service) AppByPK(ctx context.Context, pk int64) (*store.GitHubAppRow, error)

AppByPK loads a GitHub App by its internal primary key. The REST layer uses this to render the app object for GET /app.

func (*Service) ApproveDeviceCode

func (s *Service) ApproveDeviceCode(ctx context.Context, userCode string, userPK int64) error

ApproveDeviceCode marks the session behind userCode approved by userPK. It returns store.ErrNotFound when the code is unknown or already expired.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, authorization string) (*Actor, error)

Authenticate turns an Authorization header value into an Actor. An empty header is the anonymous actor with a nil error: public reads still work, and handlers decide whether to demand authentication. A present-but-invalid credential returns ErrBadCredentials, which the REST layer maps to 401.

func (*Service) Close

func (s *Service) Close()

Close releases the background last-used flusher.

func (*Service) CreateInstallationToken added in v0.1.3

func (s *Service) CreateInstallationToken(ctx context.Context, actor *Actor, instPK int64,
	_ []string, _ map[string]string) (plaintext string, expiresAt time.Time, err error)

CreateInstallationToken mints a ghs_ token for instPK. actor must be KindAppJWT and own the installation. On success it returns the plaintext token and expiry. repos and permissions narrow the grant (empty = full installation grant).

func (*Service) CreatePAT added in v0.1.3

func (s *Service) CreatePAT(ctx context.Context, userPK int64, note string, scopes []string) (string, error)

CreatePAT mints a classic personal access token for userPK with the given note and scopes, persists its hash, and returns the one-time plaintext. Unknown scopes are dropped and implied children folded into their parent, the same normalization every other mint path applies.

func (*Service) DeletePAT added in v0.1.3

func (s *Service) DeletePAT(ctx context.Context, userPK, id int64) error

DeletePAT removes one of the user's personal access tokens. A pk the user does not own answers ErrPATNotFound, indistinguishable from one that never existed.

func (*Service) DenyDeviceCode

func (s *Service) DenyDeviceCode(ctx context.Context, userCode string) error

DenyDeviceCode marks the session behind userCode denied.

func (*Service) EnsureFirstPartyApps added in v0.1.3

func (s *Service) EnsureFirstPartyApps(ctx context.Context) error

EnsureFirstPartyApps seeds the OAuth app rows first-party clients expect to exist, today just the gh CLI's device-flow app. It is idempotent and runs at every startup, so an existing row (including one another process inserted concurrently) is left alone. The gh app is a public client: it holds no client secret and signs in through the device flow alone.

func (*Service) ExchangeAuthCode added in v0.1.3

func (s *Service) ExchangeAuthCode(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*IssuedToken, error)

ExchangeAuthCode exchanges an authorization code for an OAuth user token. clientID must match the code's registered app, clientSecret must verify against the app's stored secret hash (an app with no secret on file is a public client, like the seeded gh app, and skips the check), and redirectURI must equal the redirect_uri used when the code was issued and sit under the app's registered callback.

func (*Service) GenerateAuthCode added in v0.1.3

func (s *Service) GenerateAuthCode(ctx context.Context, req CodeRequest) (string, error)

GenerateAuthCode creates a new single-use authorization code for the OAuth web flow. It returns the opaque plaintext code the server embeds in the redirect to the client's redirect_uri.

func (*Service) GenerateOAuthAuthCode added in v0.1.3

func (s *Service) GenerateOAuthAuthCode(ctx context.Context, clientID, redirectURI, scope string, userPK int64) (string, error)

GenerateOAuthAuthCode is the flat-parameter form used by the FE handler interface to avoid a circular import.

func (*Service) InstallationByAppAndAccount added in v0.1.3

func (s *Service) InstallationByAppAndAccount(ctx context.Context, appPK, accountPK int64) (*store.InstallationRow, error)

InstallationByAppAndAccount resolves the installation of app appPK on the account accountPK, backing GET /repos/{owner}/{repo}/installation.

func (*Service) InstallationByDBID added in v0.1.3

func (s *Service) InstallationByDBID(ctx context.Context, dbID int64) (*store.InstallationRow, error)

InstallationByDBID loads one installation by its public database id, the id carried in the access_tokens_url the installation object hands to API clients.

func (*Service) InstallationByPK added in v0.1.3

func (s *Service) InstallationByPK(ctx context.Context, pk int64) (*store.InstallationRow, error)

InstallationByPK loads one installation by its internal primary key. The REST layer renders it for the installation-token actor's own metadata.

func (*Service) InstallationRepoPKs added in v0.1.3

func (s *Service) InstallationRepoPKs(ctx context.Context, instPK int64) ([]int64, error)

InstallationRepoPKs returns the repo PKs a "selected"-scope installation may access, backing GET /installation/repositories.

func (*Service) InstallationsByApp added in v0.1.3

func (s *Service) InstallationsByApp(ctx context.Context, appPK int64) ([]*store.InstallationRow, error)

InstallationsByApp returns all installations for the given app PK.

func (*Service) ListPATs added in v0.1.3

func (s *Service) ListPATs(ctx context.Context, userPK int64) ([]PATInfo, error)

ListPATs returns the user's live personal access tokens, newest first.

func (*Service) OAuthAppName added in v0.1.3

func (s *Service) OAuthAppName(ctx context.Context, clientID string) (string, bool)

OAuthAppName returns the display name for the OAuth app identified by clientID, and whether the app is registered. Used by the FE consent page.

func (*Service) PollDeviceToken

func (s *Service) PollDeviceToken(ctx context.Context, clientID, deviceCode string) (*DeviceTokenOutcome, error)

PollDeviceToken advances the device-flow state machine for one poll. The returned error is non-nil only on a genuine failure (a bad client, an unknown device code, or a store error); every protocol condition is reported in the outcome.

func (*Service) RequestDeviceCode

func (s *Service) RequestDeviceCode(ctx context.Context, clientID, scopeParam string) (*DeviceCodeResult, error)

RequestDeviceCode opens a device-flow session for the given client and scopes.

type Store

type Store interface {
	// Credential resolution.
	TokenByHash(ctx context.Context, hash []byte) (*store.TokenRow, error)
	UserByPK(ctx context.Context, pk int64) (*store.UserRow, error)
	BumpTokenLastUsed(ctx context.Context, at map[int64]time.Time) error

	// Personal access token management (the settings tokens page).
	TokensForUser(ctx context.Context, userPK int64) ([]*store.TokenRow, error)
	DeleteUserToken(ctx context.Context, pk, userPK int64) error

	// OAuth device flow.
	OAuthAppByClientID(ctx context.Context, clientID string) (*store.OAuthAppRow, error)
	InsertOAuthApp(ctx context.Context, a *store.OAuthAppRow) error
	InsertToken(ctx context.Context, t *store.TokenRow) error
	InsertDeviceCode(ctx context.Context, d *store.DeviceCodeRow) error
	DeviceCodeByHash(ctx context.Context, hash []byte) (*store.DeviceCodeRow, error)
	DeviceCodeByUserCode(ctx context.Context, userCode string) (*store.DeviceCodeRow, error)
	SetDeviceState(ctx context.Context, pk int64, state string, userPK int64) error
	SetDeviceInterval(ctx context.Context, pk int64, interval int) error
	SetDevicePolled(ctx context.Context, pk int64, at time.Time) error
	DeleteDeviceCode(ctx context.Context, pk int64) error

	// OAuth web flow (authorization code grant).
	InsertAuthCode(ctx context.Context, a *store.AuthCodeRow) error
	ConsumeAuthCode(ctx context.Context, codeHash []byte) (*store.AuthCodeRow, error)

	// GitHub App auth.
	GitHubAppByPK(ctx context.Context, pk int64) (*store.GitHubAppRow, error)
	InstallationByPK(ctx context.Context, pk int64) (*store.InstallationRow, error)
	InstallationByDBID(ctx context.Context, dbID int64) (*store.InstallationRow, error)
	InstallationByAppAndAccount(ctx context.Context, appPK, accountPK int64) (*store.InstallationRow, error)
	InstallationsByAppPK(ctx context.Context, appPK int64) ([]*store.InstallationRow, error)
	InstallationRepoPKs(ctx context.Context, instPK int64) ([]int64, error)
}

Store is the narrow slice of the metadata store the auth package depends on. *store.Store satisfies it. Keeping the dependency to an interface lets the auth tests drive the service with an in-memory fake and documents exactly which store methods auth reaches for.

Jump to

Keyboard shortcuts

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