auth

package
v0.7.1 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package auth provides the reusable OAuth and bearer-token contracts an application plugs into when connecting to a network MCP server.

The package is a leaf: it depends on the standard library and on the module's dependency-free internal/limits helpers, and on nothing else in the module. It defines seams, not policy — where tokens live (TokenStore), how a browser is opened (BrowserOpener), how non-OAuth credentials are supplied (HeaderProvider), what an auth posture looks like from outside (Status), and how auth failures are classified (Class). The OAuth flow that drives these seams, and the transports that consume them, live elsewhere; nothing here speaks HTTP.

Secrets

The module's standing rule is that token values, client secrets, authorization codes, verifiers, and bearer headers never enter events, catalogs, fingerprints, or logs. This package is where that rule is mechanized, because it is the only package that holds such material.

Every type here that holds a secret — TokenSet, Header — keeps it in an unexported field and exposes it only through a named accessor, so that:

  • no reflection-based encoder can reach it: encoding/json refuses via MarshalJSON, and encoding/gob refuses because there is nothing exported to encode;
  • fmt renders it redacted for every verb, because these types implement fmt.Formatter — Stringer alone would cover only %v, %s, %q, %x and %X and let %d and friends fall through to reflection, which reads unexported fields;
  • reading it is a deliberate, greppable act.

Values are non-secret metadata (expiry, scopes, header names, auth state) and are exported normally: Status in particular is designed to be logged as-is.

The dividing line is that leaking must require intent. Accessors make intentional use easy and accidental use hard.

This holds even where methods cannot reach. fmt renders a value held in another struct's unexported field by reflection, skipping Formatter, Stringer and GoStringer alike, and %p and %w bypass those methods outright — so the secrets additionally sit behind a pointer, which fmt's reflection prints as an address rather than following. Redaction is therefore a property of the layout, not only of the methods.

Index

Constants

View Source
const (
	// DefaultAuthorizationTimeout bounds how long the flow waits for the user
	// to finish in the browser. It is generous because the user may have to
	// find a password manager, complete an MFA challenge, or approve on a
	// phone; it is bounded because a flow that waits forever holds a loopback
	// port and a mutex forever.
	DefaultAuthorizationTimeout = 5 * time.Minute
	// DefaultHTTPTimeout bounds each individual HTTP request the flow makes.
	// Every one of them is a small JSON round trip to an endpoint that is
	// either healthy or not.
	DefaultHTTPTimeout = 30 * time.Second
)

Defaults for an OAuthConfig that does not state otherwise.

View Source
const (
	// MaxOriginBytes bounds ServerOrigin.
	MaxOriginBytes = 512
	// MaxClientIDBytes bounds ClientID.
	MaxClientIDBytes = 256
)

Bounds on the components of a Key. Both are generous for legitimate values and exist to keep a Key's rendering — which goes to logs — bounded.

View Source
const ExpirySkew = 30 * time.Second

ExpirySkew is subtracted from a token's expiry when deciding whether it is still usable. It covers the round trip between deciding to use a token and the server validating it, plus clock drift between the two — a token that is valid for another two seconds is not worth sending, because the request will land after it dies and cost a retry.

Thirty seconds is the conventional value and is deliberately larger than any plausible request latency, since refreshing early is cheap and being rejected mid-operation is not.

View Source
const MaxMessageBytes = 256

MaxMessageBytes bounds every message this package renders — Error.Msg and Status.Failure alike. It is deliberately smaller than client.MaxMessageBytes: auth messages are classifications written by this module, not server text relayed through it, so there is nothing legitimate to say at length.

View Source
const MaxURLBytes = 2048

MaxURLBytes bounds the input to CanonicalOrigin.

It is larger than MaxOriginBytes because the input is a whole URL — the caller's real server URL, path and query included — while the output is only its origin. The output is bounded separately, against MaxOriginBytes, so a long path cannot produce an over-long Key.

View Source
const Redacted = "[REDACTED]"

Redacted is the text that stands in for secret material in every rendering this package produces.

Variables

View Source
var ErrMarshalRefused = errors.New("refusing to marshal secret material; use the explicit accessors to persist it")

ErrMarshalRefused reports that a value holding secret material refused to serialize itself. See the file comment for why refusal beats redaction here; use the explicit accessors to persist a token deliberately.

View Source
var ErrNoToken = errors.New("no token stored")

ErrNoToken reports that a token store holds no token for a key. It is the contract that separates "absent" from "failed": a store that cannot tell the difference forces its caller to treat every read failure as a reason to start an interactive login, which is neither fail-closed nor usable.

TokenStore implementations must return an error that satisfies errors.Is(err, ErrNoToken) for an absent key, and must not use it for any other condition. NewNoTokenError builds a conforming value.

Functions

func CanonicalOrigin

func CanonicalOrigin(rawURL string) (string, error)

CanonicalOrigin reduces rawURL to the canonical origin a Key requires: scheme://host[:port], lowercase, with no default port, path, query, fragment, or userinfo. This is RFC 6454 origin serialization.

Violations are returned as *Error with class ClassInvalidConfig. The result, when err is nil, always satisfies Key.Validate and is idempotent under a second call.

What it normalizes, and why each is safe to do silently: these are all cases where two spellings are provably the same origin, so normalizing costs the caller nothing and NOT normalizing costs a duplicate store entry and a redundant interactive login.

HTTPS://Example.COM./mcp?q=1#f  ->  https://example.com
https://example.com:0443        ->  https://example.com
http://127.0.0.1:8080/mcp       ->  http://127.0.0.1:8080

What it refuses, and why each is NOT safe to normalize:

  • userinfo — "https://user:pw@h" carries a credential. Silently dropping it would discard something the caller meant, and keeping it is not an origin. Only the caller knows which it wanted, so it must say.
  • a non-ASCII host — converting to punycode needs golang.org/x/net/idna, which is not a sanctioned dependency; and a Unicode homograph reaching a log line through Key.String is its own problem. The caller encodes the A-label, because the caller is what knows the name.
  • an IPv6 zone identifier — scoped to one machine's interfaces, so not an identity a token can be keyed by.
  • http to a non-loopback host — tokens do not cross a cleartext network. This mirrors Key.Validate and what the HTTP transport will require.

Types

type BrowserOpener

type BrowserOpener interface {
	// OpenURL presents url to the user, or returns an error if it cannot.
	OpenURL(ctx context.Context, url string) error
}

BrowserOpener opens a URL in the user's browser, so an OAuth authorization-code flow can reach the resource owner.

The module supplies no implementation: what "open a browser" means is a property of the application. A desktop app shells out to the platform opener, a TUI prints the URL for the user to copy, an SSH session may only be able to do the latter, and a headless service should refuse outright rather than block on a human who is not there. That last case is why this is an interface and not a helper: refusing is a legitimate implementation.

Implementations must honor ctx, and must return promptly — opening a browser means handing the URL off, never waiting for the user to finish.

The URL carries authorization parameters. It is not for logging.

type Class

type Class uint8

Class classifies an auth failure so callers can branch on what went wrong without parsing error text. The zero value is not a valid class.

const (
	// ClassInvalidConfig is a malformed key, header, or auth configuration.
	// It is a programmer or operator error, not a protocol outcome.
	ClassInvalidConfig Class = iota + 1
	// ClassNoToken is a token store reporting that it holds no token for a
	// key. It means "absent", never "broken" — see ErrNoToken.
	ClassNoToken
	// ClassRequired is a server demanding credentials the client does not
	// have.
	ClassRequired
	// ClassDenied is an authorization request the resource owner or server
	// refused.
	ClassDenied
	// ClassExpired is a token past its expiry that could not be refreshed.
	ClassExpired
	// ClassFailed is any other auth failure: discovery, registration, or a
	// refresh that broke rather than being refused.
	ClassFailed
)

Auth failure classes. Values are contiguous starting at 1; the zero value is reserved as "no class".

func ClassOf

func ClassOf(err error) (Class, bool)

ClassOf walks err's chain and reports the class of the outermost *Error. It returns false when the chain contains no *Error.

func (Class) String

func (c Class) String() string

String returns a stable lowercase snake_case identifier for the class. Undeclared values return "unknown".

type ClientCredentials

type ClientCredentials struct {
	// contains filtered or unexported fields
}

ClientCredentials is an OAuth client's identity: a public client identifier and, for a confidential client, the secret that authenticates it.

The secret is reachable only through Secret(). Construct with NewClientCredentials. The zero value is an unregistered client — Valid reports false — which is what an OAuthConfig carries when it wants dynamic registration.

A ClientCredentials is a value and is safe to copy; it is immutable after construction.

func NewClientCredentials

func NewClientCredentials(id, secret string) ClientCredentials

NewClientCredentials builds a ClientCredentials. A public client — which is what a native application using PKCE is — has no secret, so an empty secret is normal and valid.

func (ClientCredentials) Confidential

func (c ClientCredentials) Confidential() bool

Confidential reports whether the client authenticates with a secret.

func (ClientCredentials) Format

func (c ClientCredentials) Format(f fmt.State, verb rune)

Format routes every fmt verb through the redacted rendering; see TokenSet.Format for why Stringer alone is insufficient.

func (ClientCredentials) GoString

func (c ClientCredentials) GoString() string

GoString renders redacted text for a direct caller; Format is what serves %#v. See TokenSet.GoString.

func (ClientCredentials) ID

func (c ClientCredentials) ID() string

ID returns the client identifier, which is not secret.

func (ClientCredentials) MarshalJSON

func (c ClientCredentials) MarshalJSON() ([]byte, error)

MarshalJSON always fails; see TokenSet.MarshalJSON for why refusing beats redacting. A caller persisting credentials uses ID() and Secret().

func (ClientCredentials) Secret

func (c ClientCredentials) Secret() string

Secret returns the client secret, which is empty for a public client. This is secret material: it goes in a token request's Authorization header and nowhere else.

func (ClientCredentials) String

func (c ClientCredentials) String() string

String renders the credentials with the secret redacted and the ID shown; see Key.String for why showing a client ID is correct.

func (*ClientCredentials) UnmarshalJSON

func (c *ClientCredentials) UnmarshalJSON([]byte) error

UnmarshalJSON always fails; see TokenSet.UnmarshalJSON.

func (ClientCredentials) Valid

func (c ClientCredentials) Valid() bool

Valid reports whether these credentials name a client at all. It says nothing about whether the authorization server still knows that client.

type Error

type Error struct {
	// Class states what kind of auth failure occurred.
	Class Class
	// Op names the operation that failed (e.g. "load", "refresh").
	Op string
	// Msg is a bounded, normalized, secret-free description.
	Msg string
	// Err is the wrapped cause, if any. It is never rendered — not through
	// Error, not through any fmt verb, and not through a copy of this struct.
	// See the receivers on Error's methods: they are VALUES, deliberately.
	Err error
}

Error is the package's operational error. Msg is already normalized and bounded (NewError enforces this); construct values with NewError rather than a composite literal so the bound holds.

func NewError

func NewError(class Class, op string, msg string, wrapped error) *Error

NewError builds an *Error with msg normalized (control characters replaced by spaces, invalid UTF-8 repaired) and bounded to MaxMessageBytes.

msg must not contain secret material: it is the only caller-supplied text this package renders, and it is rendered verbatim.

func NewNoTokenError

func NewNoTokenError(op string) *Error

NewNoTokenError builds the error a TokenStore must return for an absent key: class ClassNoToken, wrapping ErrNoToken so errors.Is matches.

func (Error) Error

func (e Error) Error() string

Error renders "auth: <op>: <class>: <msg>", omitting empty segments. The wrapped cause is never rendered — see the file comment.

func (Error) Format

func (e Error) Format(f fmt.State, verb rune)

Format routes every fmt verb through Error, so that the decision not to render the wrapped cause actually holds.

Without this, the decision is only skin deep: fmt consults the error interface for %v, %s, %q, %x and %X, but sends every other verb to reflection — which walks the struct and prints the Err field's contents in full. `fmt.Sprintf("%d", err)` on an Error wrapping an HTTP failure would print whatever that failure's text contains, which is exactly the material Error refuses to render through Error(). A verb typo must not be the difference.

The VALUE receiver is the other half, and it is not a style choice — it is the same reasoning TokenSet's receivers follow. A pointer receiver puts Format in *Error's method set only, so a value copy — `fmt.Sprintf("%v", *err)`, an Error stored or ranged by value, a struct embedding one — misses Formatter entirely and falls to reflection, which prints Err in full. This package hands callers a *Error, but "never rendered" is a claim about the TYPE, and a caller who copies one has done nothing wrong. A value receiver covers both method sets, so the claim is true of every value of this type rather than only of the pointers this package happens to return.

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap returns the wrapped cause for errors.Is / errors.As traversal.

type Header struct {
	// contains filtered or unexported fields
}

Header is one credential header.

Value is secret and is reachable only through Value(); Name is not. Construct one with NewHeader. The zero value is an invalid header — Validate rejects it — and renders as redacted like any other.

func NewHeader

func NewHeader(name, value string) Header

NewHeader builds a Header. Call Validate before using one built from configuration or from any other untrusted source.

func (Header) Format

func (h Header) Format(f fmt.State, verb rune)

Format routes every fmt verb through the redacted rendering; see TokenSet.Format for why Stringer alone is insufficient.

func (Header) GoString

func (h Header) GoString() string

GoString renders redacted text for a direct caller; Format is what serves %#v. See TokenSet.GoString.

func (Header) MarshalJSON

func (h Header) MarshalJSON() ([]byte, error)

MarshalJSON always fails, for the reason TokenSet.MarshalJSON does: a header reaching a JSON encoder is a leak, and refusing is the loud answer.

func (Header) Name

func (h Header) Name() string

Name returns the header's field name, which is not secret.

func (Header) String

func (h Header) String() string

String renders the header with its value redacted.

func (*Header) UnmarshalJSON

func (h *Header) UnmarshalJSON([]byte) error

UnmarshalJSON always fails; see TokenSet.UnmarshalJSON.

func (Header) Validate

func (h Header) Validate() error

Validate reports whether h is a well-formed HTTP header. Violations are returned as *Error with class ClassInvalidConfig.

This is a header-injection check, not a style check. A newline in a value, or a colon or space in a name, lets whoever controls that string append headers of their own to the request — so the check is against the RFC 9110 grammar (token for the name, visible ASCII plus space and tab for the value) and fails closed on anything outside it.

An empty value is allowed: it is a legal header, and a provider legitimately emits one to unset something. An empty name is not.

func (Header) Value

func (h Header) Value() string

Value returns the header's value. This is secret material: write it to a request, and nothing else.

type HeaderProvider

type HeaderProvider interface {
	// Headers returns the headers to attach, or an error if they cannot be
	// obtained. An empty result is valid and means "add nothing".
	Headers(ctx context.Context) ([]Header, error)
}

HeaderProvider supplies credential headers for an outbound request. It is the seam for everything that is not the OAuth flow this package drives: a static bearer token from an environment variable, an API key, a signed header from a cloud credential chain, or a token minted by an application's own broker.

Headers is called per request rather than once per connection, so an implementation backed by an expiring credential can refresh transparently. Implementations must honor ctx — minting a credential may mean I/O — and must be safe for concurrent use, since a connection may have several requests in flight.

Returned values are secret. A transport writes them onto a request and nothing else: they never reach an event, a log, or an error.

type Key

type Key struct {
	// ServerOrigin is the protected resource's identity as a canonical
	// origin: scheme://host[:port], lowercase, with no default port, path,
	// query, fragment, or userinfo. This is RFC 6454 origin serialization.
	ServerOrigin string
	// ClientID is the OAuth client identifier the tokens were issued to. It
	// is empty before dynamic client registration has run — an unregistered
	// client legitimately has no ID yet — so Validate accepts an empty value.
	// It is part of the key because the same server, reached by two registered
	// clients, must not share a token.
	ClientID string
}

Key identifies the tokens held for one protected resource as accessed by one client. Both fields are non-secret identifiers and are safe to log.

A Key is a cache key, so ServerOrigin must be canonical: two spellings of the same origin would silently become two entries, and the second would trigger a redundant interactive login. Validate therefore rejects non-canonical spellings rather than normalizing them — a store keyed by a value the caller did not choose is a surprising store. Callers canonicalize when they build the Key.

func (Key) String

func (k Key) String() string

String renders the key for logs as "<origin>#<client-id>", with "-" standing in for an unregistered client.

ClientID is included deliberately. RFC 6749 §2.2 is explicit that a client identifier "is not a secret; it is exposed to the resource owner" — it is a public identifier, unlike the client secret that may accompany it. Including it is what makes two keys for the same server distinguishable in a log, which is exactly when someone is reading these lines. Validate has already bounded both fields and rejected control characters, so the result is safe to embed in a log line.

func (Key) Validate

func (k Key) Validate() error

Validate reports whether k is a well-formed, canonical key. Violations are returned as *Error with class ClassInvalidConfig.

The scheme rule mirrors what the HTTP transport will require: https everywhere, with http tolerated only for loopback, where there is no network to eavesdrop on and where local development and OAuth redirect listeners actually live.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-memory TokenStore: the reference implementation of the contract, the store tests use, and the right choice for an application that wants tokens to die with the process.

It is safe for concurrent use. The zero value is not usable; call NewMemoryStore.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(ctx context.Context, key Key) error

Delete implements TokenStore.

func (*MemoryStore) Format

func (s *MemoryStore) Format(f fmt.State, verb rune)

Format routes every fmt verb through the redacted rendering.

A store needs this for a reason its contents do not: fmt skips the methods of a value it reaches through an *unexported* field, and the token map is one. Without Format, an unhandled verb would walk straight past TokenSet's own Format into the map and render every entry. Holding secrets transitively is the same obligation as holding them directly.

func (*MemoryStore) GoString

func (s *MemoryStore) GoString() string

GoString renders redacted text for a direct caller; Format is what serves %#v. See TokenSet.GoString.

func (*MemoryStore) Load

func (s *MemoryStore) Load(ctx context.Context, key Key) (TokenSet, error)

Load implements TokenStore.

func (*MemoryStore) Store

func (s *MemoryStore) Store(ctx context.Context, key Key, set TokenSet) error

Store implements TokenStore.

func (*MemoryStore) String

func (s *MemoryStore) String() string

String renders the store without its contents. A map of tokens is exactly the thing that should never be printed, and MemoryStore is a plausible field of some larger struct that someone will log.

type OAuthConfig

type OAuthConfig struct {
	// ServerURL is the MCP server this provider gets tokens for: the protected
	// resource. It is canonicalized to an origin (see CanonicalOrigin) to key
	// the token store, and used as the RFC 8707 resource indicator.
	ServerURL string

	// Credentials identifies the OAuth client. A zero value means the client is
	// unregistered, and the provider will attempt dynamic client registration
	// (RFC 7591) against the authorization server if it offers it.
	//
	// Registration is not persisted — this package stores nothing but tokens.
	// Read OAuthProvider.Credentials after a successful flow and persist the
	// result if repeat registration is undesirable, which on most servers it is.
	Credentials ClientCredentials

	// Scopes are the scopes to request. When empty, the scopes the resource
	// advertises are requested, and when it advertises none, no scope parameter
	// is sent at all — which asks the server for its default.
	Scopes []string

	// Store is where tokens are kept. Required: a provider with nowhere to put
	// a token would run an interactive flow on every call, which is not a
	// degraded mode worth having. Use NewMemoryStore for tokens that should die
	// with the process.
	Store TokenStore

	// Browser opens the authorization URL. Required, because there is no
	// sensible default: see BrowserOpener.
	Browser BrowserOpener

	// HTTPClient is used for discovery, registration, and token requests. When
	// nil, a client with explicit timeouts and TLS 1.2 minimum is used.
	//
	// A supplied client is used as-is, timeouts and TLS settings included. That
	// is the caller's call to make: the reason to inject one is usually a
	// corporate proxy or a pinned root, and second-guessing it here would
	// defeat the point.
	HTTPClient *http.Client

	// ClientName is the human-readable name registered with the authorization
	// server, shown to the user on the consent screen. Defaults to
	// "mcp-client".
	ClientName string

	// AuthorizationTimeout bounds the wait for the user to complete the browser
	// flow. Defaults to DefaultAuthorizationTimeout.
	AuthorizationTimeout time.Duration
}

OAuthConfig configures an OAuthProvider. Build the provider with NewOAuthProvider, which validates and applies defaults.

Note that there is no ClientSecret field. A client secret lives inside Credentials, behind an accessor, so that an OAuthConfig — a value an application builds at startup and is entirely likely to log — carries no printable secret. See register.go.

type OAuthProvider

type OAuthProvider struct {
	// contains filtered or unexported fields
}

OAuthProvider obtains and maintains OAuth tokens for one MCP server.

It implements HeaderProvider, so an HTTP transport can ask it for an Authorization header per request and stay ignorant of OAuth entirely.

It is safe for concurrent use, and concurrent callers cooperate: the first to need a token runs the flow and the rest wait for its result, rather than each opening a browser. Build one with NewOAuthProvider; the zero value is not usable.

func NewOAuthProvider

func NewOAuthProvider(cfg OAuthConfig) (*OAuthProvider, error)

NewOAuthProvider validates cfg and builds a provider.

It does no I/O: nothing is discovered, registered, or fetched until Token is called. Construction failing means the configuration is wrong, which is a programmer or operator error and is reported as ClassInvalidConfig — never as an auth failure, because nothing has been attempted yet.

func (*OAuthProvider) Credentials

func (p *OAuthProvider) Credentials() ClientCredentials

Credentials returns the OAuth client credentials in effect, including any assigned by dynamic registration.

This is how a caller persists a registration: read it after a successful Token, keep the ID and Secret, and pass them back through OAuthConfig next run. Without that, every process start registers a new client on the authorization server.

func (*OAuthProvider) Headers

func (p *OAuthProvider) Headers(ctx context.Context) ([]Header, error)

Headers implements HeaderProvider, returning the bearer Authorization header for a request.

This is the whole interface between OAuth and the HTTP transport: the transport calls this per request and never learns that OAuth exists.

func (*OAuthProvider) Status

func (p *OAuthProvider) Status() Status

Status returns the provider's last known auth posture.

It never blocks on a flow in progress and never performs I/O: it reports what the last completed operation established. A Status is designed to be logged as-is; see status.go.

func (*OAuthProvider) Token

func (p *OAuthProvider) Token(ctx context.Context) (TokenSet, error)

Token returns a valid access token, refreshing or running the full authorization flow as needed.

It may block for a long time: the full flow waits for a human. Callers who cannot wait should pass a ctx with a deadline — the flow honors it at every step, including the wait for the browser.

type State

type State uint8

State is a binding's auth posture. The zero value is not a valid state.

const (
	// StateAnonymous is a binding with no auth configured. It is not a
	// failure: plenty of servers need no credentials.
	StateAnonymous State = iota + 1
	// StateRequired is a binding that needs credentials it does not have.
	// This is the state that warrants an interactive login.
	StateRequired
	// StateAuthenticated is a binding holding a usable token.
	StateAuthenticated
	// StateExpired is a binding whose token has lapsed. It is separate from
	// StateRequired because it may be recoverable without the user: a refresh
	// token, if there is one, resolves it silently.
	StateExpired
	// StateDenied is a binding whose authorization was refused. It is
	// separate from StateFailed because retrying will not help — the answer
	// was "no", not "something broke".
	StateDenied
	// StateFailed is a binding whose auth broke: discovery, registration, or
	// a refresh that errored rather than being refused.
	StateFailed
)

The auth states. Values are contiguous starting at 1; the zero value is reserved as "no state".

The set is deliberately small and mirrors the Class taxonomy: every state a caller can act on differently, and no state it cannot. In particular there is no "authenticating" state here — a binding being mid-flow is a lifecycle fact, owned by the connection's own state machine (see internal/lifecycle's StateAuthenticating), not a property of the credentials.

func (State) String

func (s State) String() string

String returns a stable lowercase snake_case identifier for the state. Undeclared values return "unknown".

type Status

type Status struct {
	// State is the auth posture.
	State State
	// Expiry is when the current token lapses. Zero when there is no token or
	// the server stated no expiry.
	Expiry time.Time
	// Scopes are the scopes currently granted, if any.
	Scopes []string
	// Failure is a bounded, normalized, secret-free classification of what
	// went wrong. Empty unless State is a failure state.
	Failure string
}

Status is a snapshot of a binding's auth posture. It is a value: callers may hold, copy, log, and mutate it freely.

Build one with NewStatus, which bounds Failure and detaches Scopes. The fields are exported because this type exists to be read — by a UI, a log line, a metric — and hiding them behind accessors would buy nothing: there is no secret here to protect.

func NewStatus

func NewStatus(state State, expiry time.Time, scopes []string, failure string) Status

NewStatus builds a Status, bounding failure to MaxMessageBytes, normalizing its control characters, and cloning scopes so the Status cannot alias the caller's slice.

failure must not contain secret material: it is rendered verbatim wherever the Status goes, which is by design everywhere.

func StatusOf

func StatusOf(set TokenSet, now time.Time) Status

StatusOf derives the observable posture of a token set as of now. It is the single place the credential model and the observable model meet, so that "what does this token mean?" has one answer instead of one per caller.

It reports only what a TokenSet can prove: a set with no access token is StateRequired, a lapsed one is StateExpired, and a usable one is StateAuthenticated. StateAnonymous, StateDenied, and StateFailed are not derivable from a token — they are facts about configuration or about what a server said — so the flow that learns them builds its Status directly.

type TokenSet

type TokenSet struct {
	// contains filtered or unexported fields
}

TokenSet is one set of OAuth credentials.

Its fields are unexported and its secrets are reachable only through Access and Refresh; see the file comment for the reasoning. Construct one with NewTokenSet. The zero value is a valid, empty set: Valid reports false for it.

A TokenSet is a value and is safe to copy. It is immutable after construction, so copies cannot diverge and concurrent readers need no synchronization.

func NewTokenSet

func NewTokenSet(access, refresh string, expiry time.Time, scopes []string) TokenSet

NewTokenSet builds a TokenSet. scopes is cloned, so the caller's slice and the TokenSet cannot alias.

A zero expiry means "no known expiry": some servers issue tokens without one, and Expired treats such a token as never expiring rather than as immediately dead.

func (TokenSet) Access

func (t TokenSet) Access() string

Access returns the access token. This is secret material: send it to a server, or hand it to a store that is persisting it. Never log it, never put it in an error, never put it in an event.

func (TokenSet) Expired

func (t TokenSet) Expired(now time.Time) bool

Expired reports whether the access token is expired as of now, treating it as expired ExpirySkew early. A set with no stated expiry never expires.

func (TokenSet) Expiry

func (t TokenSet) Expiry() time.Time

Expiry returns the access token's expiry, or the zero time when the server did not state one.

func (TokenSet) Format

func (t TokenSet) Format(f fmt.State, verb rune)

Format routes every fmt verb through the redacted rendering.

Stringer is not enough on its own, and this is the subtle part of the whole design: fmt consults Stringer only for %v, %s, %q, %x and %X. Any other verb — %d, %t, %f, %c, ... — falls through to reflection, and fmt's reflection path reads unexported fields, so `fmt.Sprintf("%d", set)` would print `{%!d(string=<the token>) ...}`. A wrong verb is a typo, not a decision, and a typo must not be the difference between a redacted log line and a leaked credential.

Formatter is consulted before Stringer and before reflection, for every verb, which closes the whole class. The verb is deliberately ignored: there is no verb for which printing this value in any form other than redacted is correct.

func (TokenSet) GoString

func (t TokenSet) GoString() string

GoString renders redacted text for a direct caller.

It is not what serves %#v — Format is consulted before GoStringer, so fmt never reaches this. It is kept as defense in depth: if Format is ever removed, this becomes load-bearing again for %#v, and a caller invoking GoString itself still gets redacted text.

func (TokenSet) MarshalJSON

func (t TokenSet) MarshalJSON() ([]byte, error)

MarshalJSON always fails, so that a TokenSet reaching a JSON encoder — a log line, an event payload, an HTTP response — is a loud error rather than a silent leak or a silent loss. See the file comment.

func (TokenSet) Refresh

func (t TokenSet) Refresh() string

Refresh returns the refresh token, which is empty when the grant did not include one. This is secret material — and the more valuable of the two, since it mints access tokens. The rules for Access apply with more force.

func (TokenSet) Scopes

func (t TokenSet) Scopes() []string

Scopes returns the granted scopes as a copy; mutating it does not affect t.

func (TokenSet) String

func (t TokenSet) String() string

String renders the set with its secrets redacted, reporting only the metadata an operator needs: whether each token is present, when it expires, and what it is good for.

func (*TokenSet) UnmarshalJSON

func (t *TokenSet) UnmarshalJSON([]byte) error

UnmarshalJSON always fails, for the mirror of MarshalJSON's reason. Without it, decoding into a struct containing a TokenSet would silently produce an empty one — the same silent-credential-loss failure that argues against a redacting MarshalJSON, arriving from the other direction. A store that persists tokens reconstructs them with NewTokenSet.

func (TokenSet) Valid

func (t TokenSet) Valid() bool

Valid reports whether the set carries an access token at all. It says nothing about expiry — a caller that needs a usable token checks both, and the two are separate because an expired token with a refresh token is a different situation from no token at all.

type TokenStore

type TokenStore interface {
	// Load returns the tokens held for key, or an error wrapping ErrNoToken
	// when there are none.
	Load(ctx context.Context, key Key) (TokenSet, error)
	// Store saves tokens for key, replacing any already held.
	Store(ctx context.Context, key Key, set TokenSet) error
	// Delete removes the tokens held for key. Deleting an absent key is not
	// an error.
	Delete(ctx context.Context, key Key) error
}

TokenStore is where an application keeps tokens between runs. The module supplies no persistent implementation on purpose: a keyring, a file, a database, and a secrets manager are all legitimate, and which one is correct is a property of the application, not of MCP.

Implementations must:

  • return an error satisfying errors.Is(err, ErrNoToken) from Load when the key is absent, and use it for nothing else;
  • treat Delete of an absent key as success — the caller's intent already holds;
  • honor ctx, since a real store does I/O;
  • be safe for concurrent use.

A TokenSet handed to Store or returned from Load is a value; implementations persist it through the Access/Refresh accessors, which is the deliberate, auditable path to token material.

Jump to

Keyboard shortcuts

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