mcpauth

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 12, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

mcp-auth

CI Go Coverage License

OAuth 2.1 authorization server library for Go, designed for MCP (Model Context Protocol) servers.

Provides a complete OAuth 2.1 implementation with PKCE, dynamic client registration (RFC 7591), protected resource metadata (RFC 9728), and API key authentication. Wire the handlers into your HTTP mux and bring your own storage backend.

Install

go get github.com/alexjbarnes/mcp-auth

Quick start

package main

import (
	"net/http"

	mcpauth "github.com/alexjbarnes/mcp-auth"
)

func main() {
	srv := mcpauth.New(mcpauth.Config{
		ServerURL: "https://example.com",
		Users:     mcpauth.NewMapAuthenticator(map[string]string{
			"alice": "password123",
		}),
		GrantTypes: []string{"authorization_code", "refresh_token", "client_credentials"},
	})
	defer srv.Stop()

	mux := http.NewServeMux()

	// Register all OAuth endpoints.
	srv.Register(mux)

	// Protect your application routes with the middleware.
	mux.Handle("/mcp", srv.Middleware()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		userID := mcpauth.RequestUserID(r.Context())
		w.Write([]byte("hello " + userID))
	})))

	http.ListenAndServe(":8080", mux)
}

Endpoints

srv.Register(mux) wires:

Method Path Description
GET /.well-known/oauth-protected-resource Protected resource metadata (RFC 9728)
GET /.well-known/oauth-authorization-server Authorization server metadata (RFC 8414)
POST /oauth/register Dynamic client registration (RFC 7591)
GET /oauth/authorize Authorization code flow (shows login page)
POST /oauth/authorize Authorization code flow (handles login)
POST /oauth/token Token exchange and refresh

API keys

Register API keys for service-to-service auth without the OAuth flow.

// With a prefix to distinguish from OAuth tokens.
srv := mcpauth.New(mcpauth.Config{
	ServerURL:    "https://example.com",
	APIKeyPrefix: "sk_",
})
srv.RegisterAPIKey("sk_live_abc123", "service-account")

// Without a prefix. Tokens are speculatively checked as API keys
// and fall through to OAuth validation if not found.
srv := mcpauth.New(mcpauth.Config{
	ServerURL: "https://example.com",
})
srv.RegisterAPIKey("abc123", "service-account")

// Register by pre-computed hash when you don't have the raw key.
srv.RegisterAPIKeyByHash(mcpauth.HashSecret("abc123"), "service-account")

Pre-configured clients

Register server-owned OAuth clients that bypass dynamic registration.

err := srv.RegisterPreConfiguredClient(&mcpauth.OAuthClient{
	ClientID:   "my-service",
	SecretHash: mcpauth.HashSecret("client-secret"),
	GrantTypes: []string{"client_credentials"},
})
if err != nil {
	// Grant type not supported by server configuration.
	log.Fatal(err)
}

Storage

By default everything is in-memory. For durable storage, implement the Persistence interface and pass it via Config.Persist.

type Persistence interface {
	SaveOAuthToken(token OAuthToken) error
	DeleteOAuthToken(tokenHash string) error
	AllOAuthTokens() ([]OAuthToken, error)

	SaveOAuthClient(client OAuthClient) error
	DeleteOAuthClient(clientID string) error
	AllOAuthClients() ([]OAuthClient, error)

	SaveAPIKey(hash string, key APIKey) error
	DeleteAPIKey(hash string) error
	AllAPIKeys() (map[string]APIKey, error)
}

Raw secrets (tokens, refresh tokens) are never passed to the persistence layer. Only hashes are stored.

User authentication

Implement UserAuthenticator for custom credential validation (LDAP, database, etc.).

type UserAuthenticator interface {
	ValidateCredentials(ctx context.Context, username, password string) (userID string, err error)
}

MapAuthenticator is a built-in implementation backed by a username/password map. Passwords are hashed at construction time and not retained.

users := mcpauth.NewMapAuthenticator(map[string]string{
	"alice": "password123",
	"bob":   "hunter2",
})

Optionally implement UserAccountChecker on the same type to enable per-request account validation in the middleware (disable users without revoking tokens).

Context helpers

After the middleware authenticates a request, extract identity from the context:

userID   := mcpauth.RequestUserID(r.Context())
clientID := mcpauth.RequestClientID(r.Context())
ip       := mcpauth.RequestRemoteIP(r.Context())

WithUserID injects a user ID into a context for testing:

ctx := mcpauth.WithUserID(context.Background(), "test-user")

License

Apache 2.0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func HashSecret

func HashSecret(secret string) string

HashSecret returns the hex-encoded SHA-256 hash of a secret string.

func RandomHex

func RandomHex(byteLen int) string

RandomHex generates a cryptographically random hex string of the given byte length.

func RequestClientID

func RequestClientID(ctx context.Context) string

RequestClientID returns the OAuth client ID from the context, or "".

func RequestRemoteIP

func RequestRemoteIP(ctx context.Context) string

RequestRemoteIP returns the client IP from the context, or "".

func RequestUserID

func RequestUserID(ctx context.Context) string

RequestUserID returns the authenticated user ID from the context, or "".

func WithUserID

func WithUserID(ctx context.Context, userID string) context.Context

WithUserID returns a context with the given user ID set. This is primarily useful in tests to simulate an authenticated request without going through the middleware.

Types

type APIKey

type APIKey struct {
	KeyHash   string    `json:"key_hash"`
	UserID    string    `json:"user_id"`
	CreatedAt time.Time `json:"created_at"`
}

APIKey represents a pre-configured API key for Bearer token authentication. Unlike OAuth tokens, API keys are permanent and only removed by revocation.

type Code

type Code struct {
	Code          string
	ClientID      string
	RedirectURI   string
	CodeChallenge string
	Resource      string
	UserID        string
	Scopes        []string
	ExpiresAt     time.Time
	// contains filtered or unexported fields
}

Code represents a pending authorization code. Ephemeral, never persisted. Codes start active and are marked inactive on first use. A second attempt to consume an inactive code is treated as a replay attack (RFC 6819 Section 4.4.1.1).

type Config

type Config struct {
	// ServerURL is the public-facing base URL of the server (e.g.
	// "https://example.com"). Used for issuer, metadata endpoints,
	// and audience validation.
	ServerURL string

	// Users validates credentials during the authorization code flow.
	// Required for /oauth/authorize to work.
	Users UserAuthenticator

	// Persist is the durable storage backend. Pass nil for in-memory
	// only operation (useful in tests and single-process deployments).
	Persist Persistence

	// Logger receives structured log output. Defaults to slog.Default()
	// when nil.
	Logger *slog.Logger

	// APIKeyPrefix, when non-empty, enables API key authentication in
	// the middleware. Bearer tokens starting with this prefix are
	// treated as API keys rather than OAuth access tokens.
	APIKeyPrefix string

	// LoginTitle is the heading shown on the login page.
	// Defaults to "Sign In" when empty.
	LoginTitle string

	// LoginSubtitle is the subtitle shown on the login page.
	// Defaults to "Sign in to grant access to your account." when empty.
	LoginSubtitle string

	// GrantTypes lists the grant types advertised in server metadata.
	// Defaults to ["authorization_code", "refresh_token"] when nil.
	GrantTypes []string

	// TrustedProxyHeader, when non-empty, is the HTTP header used to
	// extract the client IP address (e.g. "X-Forwarded-For"). Only set
	// this when the server runs behind a trusted reverse proxy.
	TrustedProxyHeader string

	// LoginTemplate, when non-nil, replaces the built-in login page
	// with a custom template. The template receives a LoginData struct.
	// When nil, the default built-in login page is used.
	LoginTemplate *template.Template

	// ClientSecretValidator, when non-nil, replaces the default SHA-256
	// constant-time comparison used to validate client secrets. This allows
	// callers to use bcrypt or other hashing schemes for stored secrets.
	//
	// The function receives the raw secret from the incoming request and
	// the stored hash from the OAuthClient.SecretHash field. Return true
	// if the secret matches the hash.
	//
	// When nil, the default behavior computes SHA-256(rawSecret) and
	// performs a constant-time comparison against storedHash. This is safe
	// for high-entropy generated secrets but not suitable for user-chosen
	// passwords.
	ClientSecretValidator func(rawSecret, storedHash string) bool
}

Config holds all configuration for the OAuth 2.1 authorization server.

type LoginData

type LoginData struct {
	CSRFToken           string
	ClientID            string
	ClientName          string
	RedirectURI         string
	State               string
	CodeChallenge       string
	CodeChallengeMethod string
	Scope               string
	Resource            string
	Error               string
	Title               string
	Subtitle            string
}

LoginData holds the template data passed to the login page. When providing a custom LoginTemplate via Config, your template receives this struct. All hidden form fields must be included for the OAuth flow to work correctly.

type MapAuthenticator

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

MapAuthenticator is a UserAuthenticator backed by a map of usernames to SHA-256 password hashes. Plaintext passwords are hashed during construction and not retained. Comparison uses constant-time comparison. Unknown users are compared against a dummy hash to prevent timing-based user enumeration.

func NewMapAuthenticator

func NewMapAuthenticator(users map[string]string) MapAuthenticator

NewMapAuthenticator creates a MapAuthenticator from a map of usernames to plaintext passwords. The passwords are hashed immediately and the plaintext values are not retained.

func (MapAuthenticator) ValidateCredentials

func (m MapAuthenticator) ValidateCredentials(_ context.Context, username, password string) (string, error)

ValidateCredentials checks the username and password against the map. Returns the username as the userID on success, or ("", nil) on failure.

type OAuthClient

type OAuthClient struct {
	ClientID                string   `json:"client_id"`
	ClientName              string   `json:"client_name,omitempty"`
	RedirectURIs            []string `json:"redirect_uris"`
	GrantTypes              []string `json:"grant_types,omitempty"`
	ResponseTypes           []string `json:"response_types,omitempty"`
	TokenEndpointAuthMethod string   `json:"token_endpoint_auth_method,omitempty"`
	SecretHash              string   `json:"secret_hash,omitempty"`
	IssuedAt                int64    `json:"client_id_issued_at,omitempty"`
	UserID                  string   `json:"user_id,omitempty"`
}

OAuthClient represents a dynamically registered OAuth client.

type OAuthToken

type OAuthToken struct {
	Token        string    `json:"token,omitempty"`
	TokenHash    string    `json:"token_hash"`
	Kind         string    `json:"kind,omitempty"`
	UserID       string    `json:"user_id"`
	Resource     string    `json:"resource"`
	Scopes       []string  `json:"scopes,omitempty"`
	ExpiresAt    time.Time `json:"expires_at"`
	RefreshToken string    `json:"refresh_token,omitempty"`
	RefreshHash  string    `json:"refresh_hash,omitempty"`
	ClientID     string    `json:"client_id,omitempty"`
}

OAuthToken represents an issued access or refresh token. Kind is "access" or "refresh". Raw token values (Token, RefreshToken) are transient and never persisted to disk. Only their SHA-256 hashes are stored.

type Persistence

type Persistence interface {
	SaveOAuthToken(token OAuthToken) error
	DeleteOAuthToken(tokenHash string) error
	AllOAuthTokens() ([]OAuthToken, error)

	SaveOAuthClient(client OAuthClient) error
	DeleteOAuthClient(clientID string) error
	AllOAuthClients() ([]OAuthClient, error)

	SaveAPIKey(hash string, key APIKey) error
	DeleteAPIKey(hash string) error
	AllAPIKeys() (map[string]APIKey, error)
}

Persistence defines the storage backend for tokens, clients, and API keys. Implementations must be safe for concurrent use. Pass nil to New() for in-memory-only operation (useful in tests).

type ProtectedResourceMetadata

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

ProtectedResourceMetadata is the RFC 9728 response.

type Server

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

Server is the OAuth 2.1 authorization server. Create one with New() and wire its handler methods into your HTTP mux.

func New

func New(cfg Config) *Server

New creates a new Server from the given configuration. Call Stop() when the server is no longer needed to release background resources.

func (*Server) ClientAllowsGrant

func (srv *Server) ClientAllowsGrant(clientID, grantType string) bool

ClientAllowsGrant reports whether the given client is permitted to use the specified grant type.

func (*Server) GetClient

func (srv *Server) GetClient(clientID string) *OAuthClient

GetClient returns the registered client with the given ID, or nil.

func (*Server) HandleAuthorize

func (srv *Server) HandleAuthorize() http.HandlerFunc

HandleAuthorize returns a handler for GET+POST /oauth/authorize (authorization code flow with PKCE).

func (*Server) HandleProtectedResourceMetadata

func (srv *Server) HandleProtectedResourceMetadata() http.HandlerFunc

HandleProtectedResourceMetadata returns a handler for GET /.well-known/oauth-protected-resource (RFC 9728).

func (*Server) HandleRegistration

func (srv *Server) HandleRegistration() http.HandlerFunc

HandleRegistration returns a handler for POST /oauth/register (RFC 7591 Dynamic Client Registration).

func (*Server) HandleServerMetadata

func (srv *Server) HandleServerMetadata() http.HandlerFunc

HandleServerMetadata returns a handler for GET /.well-known/oauth-authorization-server (RFC 8414).

func (*Server) HandleToken

func (srv *Server) HandleToken() http.HandlerFunc

HandleToken returns a handler for POST /oauth/token (token exchange and refresh).

func (*Server) ListAPIKeys

func (srv *Server) ListAPIKeys() []*APIKey

ListAPIKeys returns all registered API keys.

func (*Server) Middleware

func (srv *Server) Middleware() func(http.Handler) http.Handler

Middleware returns HTTP middleware that validates Bearer tokens (both OAuth access tokens and API keys) and injects user/client/IP information into the request context. Use RequestUserID(), RequestClientID(), and RequestRemoteIP() to extract the values.

func (*Server) ReconcileAPIKeys

func (srv *Server) ReconcileAPIKeys(currentHashes map[string]struct{}) int

ReconcileAPIKeys removes API keys whose hashes are not in currentHashes. Returns the number of keys removed.

func (*Server) ReconcileClients

func (srv *Server) ReconcileClients(currentClientIDs map[string]struct{}) int

ReconcileClients removes dynamically registered clients whose IDs are not in currentClientIDs. Pre-configured clients are never removed. Returns the number of clients removed.

func (*Server) Register

func (srv *Server) Register(mux *http.ServeMux)

Register wires all OAuth endpoint handlers onto the given mux:

GET  /.well-known/oauth-protected-resource
GET  /.well-known/oauth-authorization-server
POST /oauth/register
GET  /oauth/authorize
POST /oauth/authorize
POST /oauth/token

func (*Server) RegisterAPIKey

func (srv *Server) RegisterAPIKey(rawKey, userID string)

RegisterAPIKey registers a raw API key string and associates it with the given user ID. The key is hashed with SHA-256 before storage.

func (*Server) RegisterAPIKeyByHash

func (srv *Server) RegisterAPIKeyByHash(hash, userID string)

RegisterAPIKeyByHash registers an API key using a pre-computed hash. Use this when the raw key is not available and you already hold the SHA-256 hash (e.g. from HashSecret).

func (*Server) RegisterPreConfiguredClient

func (srv *Server) RegisterPreConfiguredClient(client *OAuthClient) error

RegisterPreConfiguredClient adds a pre-configured OAuth client that survives reconciliation. Use this for server-owned clients that should not be removed by ReconcileClients.

Returns an error if the client requests grant types that the server does not support.

func (*Server) RemoveClient

func (srv *Server) RemoveClient(clientID string) bool

RemoveClient removes a client by ID. Returns true if the client existed and was removed.

func (*Server) RevokeAPIKey

func (srv *Server) RevokeAPIKey(keyHash string)

RevokeAPIKey removes the API key with the given hash.

func (*Server) Stop

func (srv *Server) Stop()

Stop releases background resources (GC goroutine). Safe to call multiple times.

type ServerMetadata

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

ServerMetadata is the RFC 8414 response.

type UserAccountChecker

type UserAccountChecker interface {
	// IsAccountActive returns true if the user account is enabled.
	// Returns (false, nil) for disabled accounts.
	// Returns non-nil error only for system failures.
	IsAccountActive(ctx context.Context, userID string) (bool, error)
}

UserAccountChecker is an optional interface that, when implemented by the Users value passed to Config, enables per-request user account validation. The middleware calls IsAccountActive after successful token validation to verify the user has not been disabled since the token was issued.

If the Users value does not implement this interface, the middleware skips the check (backward compatible).

type UserAuthenticator

type UserAuthenticator interface {
	// ValidateCredentials checks username/password and returns the user ID
	// on success. Returns ("", nil) if credentials are invalid.
	// Returns non-nil error only for system failures (database down, etc.).
	ValidateCredentials(ctx context.Context, username, password string) (userID string, err error)
}

UserAuthenticator validates user credentials during the authorization code flow. Implementations may use bcrypt, SHA-256 comparison, LDAP, database lookup, or any other mechanism.

Jump to

Keyboard shortcuts

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