Documentation
¶
Overview ¶
Package auth is the family's sign-in on GitLab's OAuth provider: the person's own OAuth application, a loopback redirect on 127.0.0.1 with a random port, PKCE S256, and a refreshing token source that survives GitLab's immediate rotation (docs/architecture.md §10).
It differs from the siblings only where GitLab forces it. The application is named by its id, not a file, and is public, so no secret is sent (§2.2). A refresh revokes the old pair at once (§2.3), so refreshing happens under a cross-process lock and the new pair is stored before it is used; see TokenSource. No `resource` parameter is ever sent: one ending in /api/v4/mcp silently narrows the grant to the mcp scope (§18 row 7).
Index ¶
- Constants
- Variables
- func IsInvalidGrant(err error) bool
- func LockRefresh(ctx context.Context, path string, timeout time.Duration) (func(), error)
- func OpenBrowser(u string) error
- type Application
- func (a *Application) Exchange(ctx context.Context, code, redirectURI, verifier string) (*Grant, error)
- func (a *Application) Login(ctx context.Context, requested []string, opts LoginOptions) (*Grant, error)
- func (a *Application) Refresh(ctx context.Context, refreshToken string) (*Grant, error)
- func (a *Application) Revoke(ctx context.Context, token string) error
- func (a *Application) TokenInfo(ctx context.Context, accessToken string) (*TokenInfo, error)
- type Grant
- type LoginOptions
- type NoCredentials
- type OAuthError
- type Store
- type TokenInfo
- type TokenSource
- type TokenSourceOptions
- type TransportError
Constants ¶
const DefaultHTTPTimeout = 60 * time.Second
DefaultHTTPTimeout bounds one call to the OAuth endpoints when no timeout is given.
const DefaultRefreshMargin = time.Minute
DefaultRefreshMargin is how long before expiry a token is refreshed. GitLab's shortest configurable lifetime is 300 s (§2.3), so a minute is always less than a token lives.
Variables ¶
var ErrConfidential = errors.New(`GitLab refused the application as a public client (invalid_client). ` +
`Open the application in GitLab (user settings > Applications), untick "Confidential", save, and run ` +
"`gitlab-mcp login` again. If it is already unticked, check the application id")
ErrConfidential means GitLab refused the application as a public client. The usual cause is the "Confidential" box, which GitLab ticks by default when an application is created.
var ErrLockTimeout = errors.New("auth: another gitlab-mcp process is refreshing the sign-in and has not finished")
ErrLockTimeout means another process held the refresh lock longer than the wait allows.
ErrReauthorize means the stored sign-in no longer works: revoked, expired, or spent by a refresh whose result was lost. Only a new login fixes it, so nothing retries it.
Functions ¶
func IsInvalidGrant ¶
IsInvalidGrant reports a refusal of the grant itself: a code or a refresh token that is spent, revoked or unknown.
func LockRefresh ¶
LockRefresh takes a profile's refresh lock for a command that must not interleave with a refresh: logout, which would otherwise revoke a pair a server has just replaced and delete the store before the server saves the new one.
func OpenBrowser ¶
OpenBrowser opens u with the platform's default handler.
Types ¶
type Application ¶
type Application struct {
Instance instance.Instance
// ClientID is the application id GitLab shows. The application is
// public, so there is no secret.
ClientID string
// HTTPClient carries the transport and its proxy. It is
// copied; redirects are refused and Timeout applied.
HTTPClient *http.Client
// Timeout bounds one call. Zero means DefaultHTTPTimeout.
Timeout time.Duration
// Now is the clock; tests stub it.
Now func() time.Time
}
Application is the person's OAuth application on one instance.
func (*Application) Exchange ¶
func (a *Application) Exchange(ctx context.Context, code, redirectURI, verifier string) (*Grant, error)
Exchange trades an authorization code for a token pair.
func (*Application) Login ¶
func (a *Application) Login(ctx context.Context, requested []string, opts LoginOptions) (*Grant, error)
Login runs the loopback authorization-code flow and returns a grant that includes a refresh token.
The listener is the IP literal 127.0.0.1 on a random port, not "localhost": RFC 8252 §7.3 says the literal "avoids inadvertently listening on network interfaces other than the loopback interface", and GitLab ignores the port of a loopback IP literal when it compares redirect URIs, while it compares localhost exactly (§2.1).
func (*Application) Refresh ¶
Refresh spends a refresh token for a new pair. GitLab revokes the old pair the moment the new one exists, so the caller stores the result before it does anything else; TokenSource does.
type Grant ¶
type Grant struct {
Token *oauth2.Token
// Scopes is what GitLab granted, which can be less than was asked.
Scopes []string
}
Grant is what a token request returned.
type LoginOptions ¶
type LoginOptions struct {
// OpenBrowser is called with the authorization URL. nil uses the
// OS's default browser; a failure is not fatal, since the URL is
// printed to Out as well.
OpenBrowser func(url string) error
// NoBrowser prints the URL and does not try to open anything. It is
// what makes this flow usable over SSH, where the callback reaches
// the remote host's loopback and the browser is local.
NoBrowser bool
// Out receives the URL and progress messages. nil discards them.
Out io.Writer
// Timeout bounds the person's trip through the browser. Default 5m.
Timeout time.Duration
// Listener overrides the loopback listener (tests).
Listener net.Listener
}
LoginOptions tune the interactive flow. Zero values are sensible.
type NoCredentials ¶
type NoCredentials struct{ Reason error }
NoCredentials is the token source of a server started before anyone signed in. The server still starts, so tools/list and the schema dump work, and every call answers that login is needed.
func (NoCredentials) Invalidate ¶
func (NoCredentials) Invalidate(string)
Invalidate does nothing: there is no token to drop.
type OAuthError ¶
OAuthError is an OAuth endpoint's refusal, with the error code GitLab sent. The description is kept short and masked; it is Doorkeeper's fixed text, and never trusted to be.
func (*OAuthError) Error ¶
func (e *OAuthError) Error() string
type Store ¶
type Store interface {
Resolve() (*oauth2.Token, credentials.Source, error)
Save(*oauth2.Token) (credentials.Source, error)
}
Store is where the token pair lives between processes. *credentials.Store is one.
type TokenInfo ¶
type TokenInfo struct {
Scopes []string
// ExpiresIn is the time left; zero for a token that does not expire.
ExpiresIn time.Duration
// ApplicationID is the id of the application the token was issued to.
ApplicationID string
ResourceOwnerID int64
}
TokenInfo is Doorkeeper's view of an access token.
type TokenSource ¶
type TokenSource struct {
// contains filtered or unexported fields
}
TokenSource hands out a valid access token, refreshing it ahead of its expiry. It implements gapi.TokenSource and is safe for concurrent use.
GitLab spends a refresh token once and revokes the old pair the moment it rotates (§2.3), and Claude Desktop and Claude Code may run a server each on one profile. So a refresh happens under a cross-process file lock; inside it the store is read again first, since another process may have refreshed while this one waited; and the new pair is stored before it is used. A refusal of the refresh token is checked against the store once more before anyone is told to log in again.
func NewTokenSource ¶
func NewTokenSource(app *Application, store Store, opts TokenSourceOptions) *TokenSource
NewTokenSource builds a token source over a store. Nothing is read until the first Token call.
func (*TokenSource) Cached ¶
func (s *TokenSource) Cached() (string, error)
Cached returns the access token when it can be used without a refresh, and "" when a refresh is due. It never refreshes.
func (*TokenSource) Close ¶
func (s *TokenSource) Close()
Close waits for a refresh in progress and refuses any later one. A process that exits between GitLab rotating the pair and the store saving it leaves every process on the profile signed out, so the server closes its source before it exits.
func (*TokenSource) Invalidate ¶
func (s *TokenSource) Invalidate(rejected string)
Invalidate drops an access token GitLab refused with 401. The next Token reads the store again, where a login in another process leaves its pair, and refreshes if the store holds the refused token too.
type TokenSourceOptions ¶
type TokenSourceOptions struct {
// LockPath is the file the refresh locks: userconfig's LockPath for
// the profile, shared by every process using it.
LockPath string
// LockTimeout bounds the wait for another process's refresh. Zero
// means twice the application's timeout, so one slow refresh
// elsewhere is waited out.
LockTimeout time.Duration
// Margin is how long before expiry to refresh. Zero means
// DefaultRefreshMargin.
Margin time.Duration
// Warn receives what the person should hear about and no call
// should fail over: a rotated pair that could not be stored.
Warn func(string)
// Now is the clock; tests stub it.
Now func() time.Time
}
TokenSourceOptions tune a TokenSource. Only LockPath is required.
type TransportError ¶
type TransportError struct {
// contains filtered or unexported fields
}
TransportError is a failure to reach an OAuth endpoint at all. Its text carries no URL and no host (§9.2): the URL names the instance, and on token info it once carried the token itself.
func (*TransportError) Error ¶
func (e *TransportError) Error() string