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) 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