oauth

package
v0.1.1 Latest Latest
Warning

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

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

Documentation

Overview

Package oauth implements the deployment's OAuth 2.1 authorization server, which exists to authorize MCP clients: MCP clients discover it through the protected-resource metadata document, send the user through the authorization-code flow with PKCE, and then present the resulting audience-bound access token to the MCP endpoint.

The protocol endpoints (/oauth/authorize, /oauth/authorize/complete, /oauth/token, /oauth/register) are plain HTTP because OAuth clients do not speak ConnectRPC. The identifier every one of them is built from is derived here, from the workspace's external_url, because the issuer and the resource identifier must be byte-identical everywhere they appear.

Index

Constants

View Source
const (
	// ConsentPath is the page that shows a pending request and records the
	// decision.
	ConsentPath = "/oauth/consent"
	// SignInPath is the SPA's sign-in page; it honours a `redirect` query
	// parameter and comes back with the browser where it left off.
	SignInPath = "/login"
)

SPA paths the browser flow uses.

View Source
const MCPReadScope = "metaxisdata.mcp.read"

MCPReadScope is the single OAuth scope the MCP endpoint requires. The real authorization decision stays with the platform's IAM, per tool call; the OAuth scope only says "this token may talk to the MCP surface at all", so a second, finer-grained scope vocabulary is deliberately not invented here.

View Source
const MCPResourcePath = "/mcp"

MCPResourcePath is appended to the issuer to form the MCP resource identifier.

View Source
const ProtectedResourcePath = "/.well-known/oauth-protected-resource"

ProtectedResourcePath is where the RFC 9728 document is served. Clients also try the path-insertion form, which the router mounts as well.

Variables

View Source
var ErrExternalURLRequired = errors.New("the workspace external_url is required for the MCP endpoint")

ErrExternalURLRequired means the workspace has no external_url, so the issuer and resource identifier cannot be derived. It is deliberately not a soft failure: falling back to the request's Host header would let the audience drift with the caller and would make an issuer mix-up attack possible.

View Source
var ErrRedirectURINotRegistered = errors.New("the redirect_uri is not registered for this client")

ErrRedirectURINotRegistered is returned when an authorization request names a redirect URI the client did not register. The caller must not redirect.

Functions

func AuthServerMetadataHandler

func AuthServerMetadataHandler(stores *store.Store) http.Handler

AuthServerMetadataHandler serves the RFC 8414 document a client reads to find the authorization, token and registration endpoints.

func MatchesRedirectURI

func MatchesRedirectURI(registered []string, requested string) bool

MatchesRedirectURI reports whether the redirect_uri of an authorization request is one the client registered.

Everything except loopback must match exactly: OAuth 2.1 requires exact matching because an approximate match is an open-redirect hole (a registered "https://app.example.com/cb" must not accept "https://app.example.com/cb.evil" or "https://app.example.com.evil/cb"). Loopback is the single documented relaxation (RFC 8252 §7.3): a native client picks an ephemeral port at runtime, so a loopback URI is compared without its port — but the scheme, host and path still have to agree.

func ParseScopeList

func ParseScopeList(raw string) []string

ParseScopeList reads the OAuth `scope` parameter, which is a space-delimited list. An empty value yields no scopes rather than one empty scope.

func ProtectedResourceHandler

func ProtectedResourceHandler(stores *store.Store) http.Handler

ProtectedResourceHandler serves the RFC 9728 document that tells an MCP client which authorization server to use for this resource. It answers 404 while the surface is switched off, so a disabled deployment does not advertise one.

func RedirectWithCode

func RedirectWithCode(w http.ResponseWriter, r *http.Request, redirectURI, code, clientState, issuer string)

RedirectWithCode sends the user agent back to the client with the authorization code, the client's opaque state and — per RFC 9207 — the issuer, which is what lets a client detect a mix-up between authorization servers.

func RedirectWithError

func RedirectWithError(w http.ResponseWriter, r *http.Request, redirectURI, clientState, code, description, issuer string)

RedirectWithError sends the user agent back to the client with an RFC 6749 error, preserving the client's state. It is how the authorization endpoint reports a failure that happened after a valid client_id and redirect_uri were established; a failure before that must be rendered by the server instead, because redirecting to an unvalidated URI is exactly the open redirect this endpoint exists to avoid. RedirectWithError reports a failure to the client. The issuer travels with the error exactly as it does with a code: RFC 9207's mix-up defence has to hold on the path where a client is told no, which is a response it can reach without ever seeing a successful one.

func RegisterHandler

func RegisterHandler(stores *store.Store) http.Handler

RegisterHandler serves POST /oauth/register (RFC 7591 dynamic client registration).

The caller wires the route and the per-IP rate limiter. Registration is anonymous and creates public clients only: token_endpoint_auth_method is fixed to "none" and response_types to "code", and grant_types may name only the two grants this server implements, because the authorization-code flow requires PKCE regardless.

func RequestedScopesValid

func RequestedScopesValid(requested []string) bool

RequestedScopesValid reports whether every requested scope is one this authorization server supports. An empty request is valid: the server then grants the one scope it has.

func ValidateRedirectURI

func ValidateRedirectURI(raw string) error

ValidateRedirectURI reports whether a client may register this redirect URI. It is the registration-time half of the rule MatchesRedirectURI enforces at request time: https, or http on a loopback address, never a fragment and never a wildcard.

func VerifyPKCE

func VerifyPKCE(challenge, codeVerifier string) bool

VerifyPKCE reports whether codeVerifier produces the S256 challenge stored with an authorization request. Only S256 is supported, so the comparison is a constant-time hash comparison; a caller must never accept a plain challenge.

Types

type AuthServerMetadata

type AuthServerMetadata struct {
	Issuer                                     string   `json:"issuer"`
	AuthorizationEndpoint                      string   `json:"authorization_endpoint"`
	TokenEndpoint                              string   `json:"token_endpoint"`
	RegistrationEndpoint                       string   `json:"registration_endpoint"`
	ScopesSupported                            []string `json:"scopes_supported"`
	ResponseTypesSupported                     []string `json:"response_types_supported"`
	GrantTypesSupported                        []string `json:"grant_types_supported"`
	CodeChallengeMethodsSupported              []string `json:"code_challenge_methods_supported"`
	TokenEndpointAuthMethodsSupported          []string `json:"token_endpoint_auth_methods_supported"`
	AuthorizationResponseIssParameterSupported bool     `json:"authorization_response_iss_parameter_supported"`
}

AuthServerMetadata is the RFC 8414 document, as served. There is deliberately no jwks_uri: tokens are signed with the deployment's symmetric AUTH_SECRET and their only consumer is this same process, so no third party ever needs a public key. See docs/security-posture.md.

type Endpoints

type Endpoints struct {
	// Issuer identifies the authorization server (RFC 8414 issuer, and the RFC
	// 9207 `iss` value in authorization responses).
	Issuer string
	// Resource is the MCP endpoint's resource identifier (RFC 8707 `resource`,
	// RFC 9728 `resource`, and the JWT audience).
	Resource string
}

Endpoints are the identifiers derived from the workspace's external_url.

func CanonicalizeExternalURL

func CanonicalizeExternalURL(raw string) (Endpoints, error)

CanonicalizeExternalURL turns the workspace's external_url into the issuer and resource identifiers.

The result is canonical so that comparisons are exact: the scheme and host are lower-cased, a default port is dropped and any trailing slash is trimmed. Only loopback may use http, because OAuth 2.1 requires the authorization server's endpoints to be served over TLS.

func (Endpoints) AuthServerMetadata

func (e Endpoints) AuthServerMetadata() AuthServerMetadata

AuthServerMetadata builds the authorization server metadata document.

func (Endpoints) AuthorizationEndpoint

func (e Endpoints) AuthorizationEndpoint() string

AuthorizationEndpoint is where a client sends the user agent to ask for consent.

func (Endpoints) CompletionEndpoint

func (e Endpoints) CompletionEndpoint() string

CompletionEndpoint is where the browser goes after consent. It is not advertised in the metadata documents: clients never call it, the consent page navigates to it so that the authorization code is minted by the server and never travels through an RPC response or through page JavaScript.

func (Endpoints) ProtectedResourceMetadata

func (e Endpoints) ProtectedResourceMetadata() ProtectedResourceMetadata

ProtectedResourceMetadata builds the metadata document for these endpoints.

func (Endpoints) RegistrationEndpoint

func (e Endpoints) RegistrationEndpoint() string

RegistrationEndpoint is where a client registers itself (RFC 7591).

func (Endpoints) TokenEndpoint

func (e Endpoints) TokenEndpoint() string

TokenEndpoint is where a client exchanges an authorization code for a token.

type EndpointsFunc

type EndpointsFunc func(ctx context.Context) (Endpoints, bool, error)

EndpointsFunc reports the deployment's identifiers and whether the MCP surface is enabled at all. Handlers take one instead of reaching for the settings themselves, so a handler can be exercised without a database.

func WorkspaceEndpoints

func WorkspaceEndpoints(stores *store.Store) EndpointsFunc

WorkspaceEndpoints returns an EndpointsFunc backed by the workspace settings. It is read per request, so turning the surface off, or moving the deployment to another address, takes effect without a restart.

type ProtectedResourceMetadata

type ProtectedResourceMetadata struct {
	Resource               string   `json:"resource"`
	AuthorizationServers   []string `json:"authorization_servers"`
	ScopesSupported        []string `json:"scopes_supported"`
	BearerMethodsSupported []string `json:"bearer_methods_supported"`
	ResourceName           string   `json:"resource_name"`
}

ProtectedResourceMetadata is the RFC 9728 document, as served. It is a local struct rather than the SDK's so that the JSON this server publishes is literally what the tests read.

type Resolution

type Resolution struct {
	Endpoints Endpoints
	Enabled   bool
}

Resolution is the outcome of reading the workspace settings for one request: whether the MCP surface is served at all, and, when it is, the identifiers the OAuth documents are built from.

func ResolveEndpoints

func ResolveEndpoints(ctx context.Context, stores *store.Store) (Resolution, error)

ResolveEndpoints reads the workspace's MCP switch and derives the OAuth identifiers from its external_url.

It is read per request (the setting read is cached by the store), so turning the surface on or off, or moving the deployment to another address, takes effect without a restart.

type Server

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

Server serves the OAuth 2.1 protocol endpoints. They are plain HTTP, because OAuth clients do not speak ConnectRPC; that also means they sit outside the ConnectRPC interceptor chain and do their own authentication, rate limiting and error rendering.

func NewServer

func NewServer(config ServerConfig) *Server

NewServer returns a server for the configured deployment.

func (*Server) Audited

func (s *Server) Audited(next http.Handler) http.Handler

Audited wraps a protocol endpoint so that every request leaves a row in the ledger. These routes are plain HTTP and sit outside the ConnectRPC interceptor chain, which is where the rest of the API gets its audit trail; a client registering itself, a token being issued and a failure on either are events the ledger exists for. The authorization decision itself is recorded by the consent RPC's own interceptor.

A ledger that cannot be written is logged, never surfaced: an audit failure must not fail a request the client is entitled to make.

func (*Server) AuthorizeHandler

func (s *Server) AuthorizeHandler() http.Handler

AuthorizeHandler serves GET /oauth/authorize: it validates the request, makes sure a signed-in user is present, records a pending authorization request and sends the browser to the consent page.

func (*Server) CompletionHandler

func (s *Server) CompletionHandler() http.Handler

CompletionHandler serves GET /oauth/authorize/complete. The consent page navigates here after approval; the server mints the authorization code and redirects it to the client, so the code never travels through an RPC response or through page JavaScript.

func (*Server) TokenHandler

func (s *Server) TokenHandler() http.Handler

TokenHandler serves POST /oauth/token for the authorization-code and refresh_token grants.

The refresh token is a rotating, opaque credential: each refresh consumes the token it was presented and issues a replacement, so a copy stolen from the client's storage is worthless the moment the real holder refreshes. That is what lets an MCP client outlive its access token without sending the user through the browser flow again.

type ServerConfig

type ServerConfig struct {
	Stores *store.Store
	State  *state.State
	// Tokens applies the same token rules the ConnectRPC API uses; the browser
	// endpoints authenticate the session cookie through it.
	Tokens *auth.TokenAuthenticator
	Mode   common.ReleaseMode
	// Secret signs the access tokens issued by the token endpoint.
	Secret string
	// TrustedProxies decides whether a forwarded address may be believed.
	TrustedProxies []string
	// Endpoints resolves the deployment's issuer and resource per request.
	Endpoints EndpointsFunc
}

ServerConfig is everything the OAuth 2.1 protocol endpoints need.

Jump to

Keyboard shortcuts

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