Documentation
¶
Overview ¶
Package oauth provides GitLab-specific OAuth 2.0 support for HTTP mode.
It verifies bearer tokens against GitLab's user endpoint, caches verified identities without storing raw token material, remembers rejections so a replayed bad token costs nothing upstream, and serves the RFC 9728 Protected Resource Metadata endpoint MCP clients use to discover the GitLab authorization server for a protected resource.
HTTP Mode Flow ¶
The package participates in the HTTP transport path as follows:
HTTP request with Authorization: Bearer
|
v
RejectedTokens --- hit ---> 401, no upstream call
|
| miss
v
NewGitLabVerifier
|
+--> TokenCache hit ---> verified identity
|
v
GitLab /user (identity) and scope introspection
NewGitLabVerifier validates Bearer tokens with GitLab and stores verified identity metadata in TokenCache. A definitive rejection is remembered by RejectedTokens; an UpstreamError never is, because it says nothing about the credential.
NewProtectedResourceHandler serves OAuth Protected Resource Metadata so MCP clients can discover the GitLab authorization server associated with the requested resource URL, and advertises the scope the deployment actually requires — see RequiredScope.
Legacy PRIVATE-TOKEN headers are not normalized into Authorization here. OAuth mode is Bearer-only, so that what the WWW-Authenticate challenge advertises is exactly what is accepted, and legacy mode reads both headers directly.
Index ¶
- Constants
- func MetadataURLFor(resourceID string) string
- func NewGitLabVerifier(gitlabURL string, skipTLS bool, cacheTTL time.Duration, cache *TokenCache) auth.TokenVerifier
- func NewProtectedResourceHandler(resourceURL, gitlabURL, requiredScope string) http.Handler
- func RequiredScope(readOnly, safeMode bool) string
- type RejectedTokens
- type TokenCache
- type UpstreamError
Constants ¶
const ( ScopeAPI = "api" ScopeReadAPI = "read_api" )
GitLab API scopes this server can operate under. api permits reads and writes; read_api is enough for a deployment that never mutates, and is what such a deployment asks for so users are not made to grant more.
Variables ¶
This section is empty.
Functions ¶
func MetadataURLFor ¶ added in v2.7.1
MetadataURLFor derives the RFC 9728 §3 protected-resource metadata URL from a resource identifier: the well-known path segment is inserted between the host component and the resource's path, so https://mcp.example.com/gitlab advertises its metadata at https://mcp.example.com/.well-known/oauth-protected-resource/gitlab and a path-less resource at the bare well-known URI. The identifier is validated at config load; a parse failure here would be a programming error, so the fallback simply appends the well-known path.
func NewGitLabVerifier ¶
func NewGitLabVerifier(gitlabURL string, skipTLS bool, cacheTTL time.Duration, cache *TokenCache) auth.TokenVerifier
NewGitLabVerifier returns an auth.TokenVerifier that validates Bearer tokens by calling the GitLab /api/v4/user endpoint. Verified identities are cached in cache (if non-nil) to avoid redundant API calls.
The returned verifier populates auth.TokenInfo with:
- UserID: the GitLab user's numeric ID (as string)
- Extra["username"]: the GitLab user's login name
- Extra["token"]: the raw token (for downstream GitLab client creation)
- Expiration: now + cacheTTL (so the SDK middleware honors TTL)
func NewProtectedResourceHandler ¶
NewProtectedResourceHandler returns an http.Handler that serves RFC 9728 Protected Resource Metadata. MCP clients use this endpoint to discover the GitLab authorization server associated with this resource.
requiredScope is the GitLab scope this deployment actually needs, which a client reads from scopes_supported to build its authorization request. It is the least privilege that works: a server that can only read asks for read_api rather than making every user grant full api.
The handler is registered at /.well-known/oauth-protected-resource.
func RequiredScope ¶ added in v2.7.4
RequiredScope reports the least-privilege GitLab scope a deployment needs: read_api when no request can reach GitLab as a write, api otherwise. Safe mode counts as read-only here because it answers mutating calls with a preview instead of forwarding them.
Types ¶
type RejectedTokens ¶ added in v2.7.5
type RejectedTokens struct {
// contains filtered or unexported fields
}
RejectedTokens remembers the tokens GitLab has already refused, so a client replaying one does not cost an upstream verification call every time.
Without it, each request carrying an invalid Bearer token is relayed 1:1 to the GitLab instance. That turns a public deployment into an amplifier: unauthenticated traffic anyone can generate becomes load on someone else's API, and on gitlab.com it becomes rate-limit pressure charged to the server's own address, where it lands on the legitimate users sharing it.
Entries are keyed by the same SHA-256 digest TokenCache uses, never the raw credential — a mistyped valid token must not be left lying in memory in the clear. The map is bounded because its keys are supplied by whoever is calling: an unbounded one would trade an amplification vector for a memory exhaustion vector.
func NewRejectedTokens ¶ added in v2.7.5
func NewRejectedTokens(capacity int, ttl time.Duration) *RejectedTokens
NewRejectedTokens returns a cache holding at most capacity rejections, each for ttl. A non-positive capacity or ttl disables caching entirely: every method still works, and Contains simply never reports a hit, so a deployment that wants no negative cache loses the amplification defense rather than crashing.
func (*RejectedTokens) Cleanup ¶ added in v2.7.5
func (r *RejectedTokens) Cleanup()
Cleanup drops every expired entry. Intended for periodic maintenance, so an idle server does not hold rejections until the next request evicts them.
func (*RejectedTokens) Contains ¶ added in v2.7.5
func (r *RejectedTokens) Contains(token string) bool
Contains reports whether the token was rejected recently enough to answer from memory. An expired entry is dropped on the way out, so a caller never sees a stale rejection.
func (*RejectedTokens) Len ¶ added in v2.7.5
func (r *RejectedTokens) Len() int
Len returns the number of entries held, expired ones included.
func (*RejectedTokens) Record ¶ added in v2.7.5
func (r *RejectedTokens) Record(token string)
Record notes that GitLab rejected this token.
Only a definitive rejection belongs here. An upstream failure — a timeout, a 5xx, a 429 — says nothing about the credential, and caching one would lock out a valid token for the whole TTL over a transient outage.
type TokenCache ¶
type TokenCache struct {
// contains filtered or unexported fields
}
TokenCache is a thread-safe, TTL-based cache for verified token identities. Keys are SHA-256 hashes of raw tokens to avoid storing sensitive material.
func (*TokenCache) Cleanup ¶
func (c *TokenCache) Cleanup()
Cleanup removes all expired entries. Intended for periodic maintenance.
func (*TokenCache) Delete ¶
func (c *TokenCache) Delete(token string)
Delete is an alias for [Evict] for API ergonomics.
func (*TokenCache) Evict ¶
func (c *TokenCache) Evict(token string)
Evict removes the cache entry for the given raw token.
func (*TokenCache) Get ¶
func (c *TokenCache) Get(token string) (*auth.TokenInfo, bool)
Get returns the cached auth.TokenInfo for the given raw token if present and not expired. Expired entries are lazily evicted on read.
func (*TokenCache) Len ¶
func (c *TokenCache) Len() int
Len returns the total number of entries (including potentially expired ones).
func (*TokenCache) Put ¶
Put stores a auth.TokenInfo for the given raw token with the specified TTL.
type UpstreamError ¶ added in v2.7.5
type UpstreamError struct {
// Status is the HTTP status GitLab returned, or 0 when the request never
// produced a response at all.
Status int
// RetryAfter is the delay GitLab asked for, or 0 when it did not say.
RetryAfter time.Duration
// Err is the underlying cause.
Err error
}
UpstreamError reports that the GitLab instance could not answer the verification request, as opposed to answering that the token is bad.
The distinction is not cosmetic. Reporting a throttled or unreachable GitLab as an invalid token tells a well-behaved MCP client to discard a perfectly good credential and start a fresh authorization flow — which generates more upstream traffic at the exact moment the instance asked for less, and asks the user to re-approve an application that was never the problem.
func (*UpstreamError) Error ¶ added in v2.7.5
func (e *UpstreamError) Error() string
Error describes the failure, naming the status when GitLab answered with one and saying it was unreachable when nothing came back at all.
func (*UpstreamError) Unwrap ¶ added in v2.7.5
func (e *UpstreamError) Unwrap() error
Unwrap exposes the underlying cause to errors.Is and errors.As.