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
- Variables
- func AuthServerMetadataHandler(stores *store.Store) http.Handler
- func MatchesRedirectURI(registered []string, requested string) bool
- func ParseScopeList(raw string) []string
- func ProtectedResourceHandler(stores *store.Store) http.Handler
- func RedirectWithCode(w http.ResponseWriter, r *http.Request, ...)
- func RedirectWithError(w http.ResponseWriter, r *http.Request, ...)
- func RegisterHandler(stores *store.Store) http.Handler
- func RequestedScopesValid(requested []string) bool
- func ValidateRedirectURI(raw string) error
- func VerifyPKCE(challenge, codeVerifier string) bool
- type AuthServerMetadata
- type Endpoints
- func (e Endpoints) AuthServerMetadata() AuthServerMetadata
- func (e Endpoints) AuthorizationEndpoint() string
- func (e Endpoints) CompletionEndpoint() string
- func (e Endpoints) ProtectedResourceMetadata() ProtectedResourceMetadata
- func (e Endpoints) RegistrationEndpoint() string
- func (e Endpoints) TokenEndpoint() string
- type EndpointsFunc
- type ProtectedResourceMetadata
- type Resolution
- type Server
- type ServerConfig
Constants ¶
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.
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.
const MCPResourcePath = "/mcp"
MCPResourcePath is appended to the issuer to form the MCP resource identifier.
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 ¶
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.
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 ¶
AuthServerMetadataHandler serves the RFC 8414 document a client reads to find the authorization, token and registration endpoints.
func MatchesRedirectURI ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
AuthorizationEndpoint is where a client sends the user agent to ask for consent.
func (Endpoints) CompletionEndpoint ¶
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 ¶
RegistrationEndpoint is where a client registers itself (RFC 7591).
func (Endpoints) TokenEndpoint ¶
TokenEndpoint is where a client exchanges an authorization code for a token.
type EndpointsFunc ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.