Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

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

View Source
const DefaultHTTPTimeout = 60 * time.Second

DefaultHTTPTimeout bounds one call to the OAuth endpoints when no timeout is given.

View Source
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

View Source
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.

View Source
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.

View Source
var ErrReauthorize = errors.New("auth: the stored sign-in no longer works; run `gitlab-mcp login`")

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

func IsInvalidGrant(err error) bool

IsInvalidGrant reports a refusal of the grant itself: a code or a refresh token that is spent, revoked or unknown.

func LockRefresh

func LockRefresh(ctx context.Context, path string, timeout time.Duration) (func(), error)

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

func OpenBrowser(u string) error

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

func (a *Application) Refresh(ctx context.Context, refreshToken string) (*Grant, error)

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.

func (*Application) Revoke

func (a *Application) Revoke(ctx context.Context, token string) error

Revoke revokes a token at GitLab. Revoking the refresh token revokes the access token issued with it.

func (*Application) TokenInfo

func (a *Application) TokenInfo(ctx context.Context, accessToken string) (*TokenInfo, error)

TokenInfo reads /oauth/token/info for an access token. The token travels in the Authorization header, never in the URL.

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.

func (NoCredentials) Token

Token always fails, naming why there are no credentials.

type OAuthError

type OAuthError struct {
	Status      int
	Code        string
	Description string
}

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.

func (*TokenSource) Token

func (s *TokenSource) Token(ctx context.Context) (string, error)

Token returns a valid access token.

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

func (*TransportError) Unwrap

func (e *TransportError) Unwrap() error

Unwrap exposes the cause.

Jump to

Keyboard shortcuts

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