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
- Variables
- func HashToken(token string) [32]byte
- func VerifyChecksum(token string) bool
- func WithActor(ctx context.Context, a *Actor) context.Context
- type Actor
- type CodeRequest
- type DeviceCodeResult
- type DeviceTokenOutcome
- type Generated
- type IssuedToken
- type Kind
- type PATInfo
- type Scope
- type Scopes
- type Service
- func (s *Service) AppByPK(ctx context.Context, pk int64) (*store.GitHubAppRow, error)
- func (s *Service) ApproveDeviceCode(ctx context.Context, userCode string, userPK int64) error
- func (s *Service) Authenticate(ctx context.Context, authorization string) (*Actor, error)
- func (s *Service) Close()
- func (s *Service) CreateInstallationToken(ctx context.Context, actor *Actor, instPK int64, _ []string, ...) (plaintext string, expiresAt time.Time, err error)
- func (s *Service) CreatePAT(ctx context.Context, userPK int64, note string, scopes []string) (string, error)
- func (s *Service) DeletePAT(ctx context.Context, userPK, id int64) error
- func (s *Service) DenyDeviceCode(ctx context.Context, userCode string) error
- func (s *Service) EnsureFirstPartyApps(ctx context.Context) error
- func (s *Service) ExchangeAuthCode(ctx context.Context, clientID, clientSecret, code, redirectURI string) (*IssuedToken, error)
- func (s *Service) GenerateAuthCode(ctx context.Context, req CodeRequest) (string, error)
- func (s *Service) GenerateOAuthAuthCode(ctx context.Context, clientID, redirectURI, scope string, userPK int64) (string, error)
- func (s *Service) InstallationByAppAndAccount(ctx context.Context, appPK, accountPK int64) (*store.InstallationRow, error)
- func (s *Service) InstallationByDBID(ctx context.Context, dbID int64) (*store.InstallationRow, error)
- func (s *Service) InstallationByPK(ctx context.Context, pk int64) (*store.InstallationRow, error)
- func (s *Service) InstallationRepoPKs(ctx context.Context, instPK int64) ([]int64, error)
- func (s *Service) InstallationsByApp(ctx context.Context, appPK int64) ([]*store.InstallationRow, error)
- func (s *Service) ListPATs(ctx context.Context, userPK int64) ([]PATInfo, error)
- func (s *Service) OAuthAppName(ctx context.Context, clientID string) (string, bool)
- func (s *Service) PollDeviceToken(ctx context.Context, clientID, deviceCode string) (*DeviceTokenOutcome, error)
- func (s *Service) RequestDeviceCode(ctx context.Context, clientID, scopeParam string) (*DeviceCodeResult, error)
- type Store
Constants ¶
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.
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 ¶
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.
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.
var ErrInstallationSuspended = errors.New("auth: installation is suspended")
ErrInstallationSuspended is returned when a caller tries to mint a token for a suspended installation.
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.
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.
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.
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 ¶
HashToken returns the sha256 the store indexes tokens by. Every caller hashes through this so they agree byte-for-byte.
func VerifyChecksum ¶
VerifyChecksum validates a presented token's class prefix and CRC32 offline, so a malformed or mistyped token is rejected without a database hit.
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 ¶
ActorFrom never returns nil: a request without a stored actor is treated as anonymous.
func (*Actor) IsAuthenticated ¶
IsAuthenticated reports whether a real credential resolved.
type CodeRequest ¶ added in v0.1.3
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 ¶
GenerateToken mints a token with the given class prefix. It never touches the database; the caller persists the hash.
type IssuedToken ¶
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 ¶
NormalizeScopes keeps only known scopes, drops any implied by a held parent, then dedupes and sorts.
func ParseScopeParam ¶
ParseScopeParam splits a space- or comma-delimited scope parameter (the OAuth "scope" field accepts both) into a Scopes set.
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 ¶
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
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 ¶
ApproveDeviceCode marks the session behind userCode approved by userPK. It returns store.ErrNotFound when the code is unknown or already expired.
func (*Service) Authenticate ¶
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) 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
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 ¶
DenyDeviceCode marks the session behind userCode denied.
func (*Service) EnsureFirstPartyApps ¶ added in v0.1.3
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
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
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
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
ListPATs returns the user's live personal access tokens, newest first.
func (*Service) OAuthAppName ¶ added in v0.1.3
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.