OAuth 2.1 Authentication¶
The mcp/oauth2 package provides an OAuth 2.1 Authorization Server for authenticated MCP servers. This is required for public MCP servers that integrate with services like ChatGPT.com.
Overview¶
The OAuth2 implementation supports:
- Authorization Code Flow with PKCE (RFC 7636)
- Dynamic Client Registration (RFC 7591)
- Authorization Server Metadata (RFC 8414)
- Bearer token authentication
Quick Start¶
Enable OAuth2 when serving over HTTP:
result, err := rt.ServeHTTP(ctx, &runtime.HTTPServerOptions{
Addr: ":8080",
OAuth2: &runtime.OAuth2Options{
Users: map[string]string{
"admin": "password",
},
},
OnReady: func(r *runtime.HTTPServerResult) {
fmt.Printf("Client ID: %s\n", r.OAuth2.ClientID)
fmt.Printf("Client Secret: %s\n", r.OAuth2.ClientSecret)
},
})
Endpoints¶
When OAuth2 is enabled, these endpoints are automatically configured:
| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server |
Server metadata (RFC 8414) |
/authorize |
Authorization endpoint |
/token |
Token endpoint |
/register |
Dynamic client registration |
Configuration¶
Basic Configuration¶
OAuth2: &runtime.OAuth2Options{
// Username/password pairs for authentication
Users: map[string]string{
"user1": "password1",
"user2": "password2",
},
}
With Pre-configured Clients¶
OAuth2: &runtime.OAuth2Options{
Users: map[string]string{"admin": "password"},
// Pre-configure a client
ClientID: "my-client-id",
ClientSecret: "my-client-secret",
}
Custom Token Lifetime¶
OAuth2: &runtime.OAuth2Options{
Users: map[string]string{"admin": "password"},
TokenExpiration: 24 * time.Hour, // Default is 1 hour
}
OAuth2 Flow¶
1. Client Registration¶
Clients register dynamically:
POST /register
Content-Type: application/json
{
"client_name": "My MCP Client",
"redirect_uris": ["https://myapp.com/callback"]
}
Response:
2. Authorization¶
Redirect user to authorize:
GET /authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=https://myapp.com/callback&
code_challenge=CHALLENGE&
code_challenge_method=S256&
state=STATE
User authenticates, then redirected to:
3. Token Exchange¶
Exchange code for token:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://myapp.com/callback&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
code_verifier=VERIFIER
Response:
4. Authenticated Requests¶
Include token in MCP requests:
Server Metadata¶
Clients can discover OAuth2 configuration:
Response:
{
"issuer": "https://myserver.com",
"authorization_endpoint": "https://myserver.com/authorize",
"token_endpoint": "https://myserver.com/token",
"registration_endpoint": "https://myserver.com/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"]
}
PKCE (Proof Key for Code Exchange)¶
PKCE is required for all authorization requests:
- Generate code verifier (random string)
- Create code challenge:
BASE64URL(SHA256(code_verifier)) - Send challenge in authorization request
- Send verifier in token request
Example (Go):
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
// Generate verifier
verifier := make([]byte, 32)
rand.Read(verifier)
codeVerifier := base64.RawURLEncoding.EncodeToString(verifier)
// Create challenge
hash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(hash[:])
Custom Authentication¶
For custom authentication logic, use the OAuth2 package directly:
import "github.com/plexusone/omniskill/mcp/oauth2"
authServer, err := oauth2.New(&oauth2.Config{
Issuer: "https://myserver.com",
// Custom authenticator
Authenticator: func(username, password string) bool {
// Check against database, LDAP, etc.
return validateCredentials(username, password)
},
})
// Use with HTTP server
http.Handle("/authorize", authServer.AuthorizeHandler())
http.Handle("/token", authServer.TokenHandler())
ChatGPT.com Integration¶
For ChatGPT.com, OAuth2 is required. Configure your server:
rt.ServeHTTP(ctx, &runtime.HTTPServerOptions{
Addr: ":443",
NgrokAuthtoken: os.Getenv("NGROK_AUTHTOKEN"),
OAuth2: &runtime.OAuth2Options{
Users: map[string]string{
os.Getenv("OAUTH_USER"): os.Getenv("OAUTH_PASSWORD"),
},
},
OnReady: func(r *runtime.HTTPServerResult) {
fmt.Println("Configure in ChatGPT:")
fmt.Printf(" MCP URL: %s\n", r.PublicURL)
fmt.Printf(" Client ID: %s\n", r.OAuth2.ClientID)
fmt.Printf(" Client Secret: %s\n", r.OAuth2.ClientSecret)
},
})
Token Revocation¶
Added in v0.11.0. Server.RevocationHandler() implements RFC 7009 token revocation, mounted by default at /oauth/revoke:
srv, err := oauth2.New(&oauth2.Config{Issuer: "https://myserver.com"})
mux.Handle("/oauth/revoke", srv.RevocationHandler())
The endpoint accepts an access or refresh token via token (and optional token_type_hint) and revokes it. Per the spec, it always returns 200 OK regardless of whether the token was found, to avoid leaking token validity to callers.
External Resource Server Mode¶
Added in v0.12.0. Instead of running the built-in authorization server, a server can validate tokens issued by an external authorization server (an enterprise IdP, or an ID-JAG / MCP Enterprise-Managed Authorization deployment) by implementing oauth2.TokenVerifier:
Implementations own all protocol-specific verification — signature checks against the issuer's JWKS, issuer/audience validation, expiry, and claim extraction. Wire it up with ExternalAuth instead of OAuth2:
result, err := rt.ServeHTTP(ctx, &runtime.HTTPServerOptions{
Addr: ":8080",
ExternalAuth: &runtime.ExternalAuthOptions{
Verifier: myJWTVerifier,
AuthorizationServers: []string{"https://idp.example.com"},
ScopesSupported: []string{"mcp:read", "mcp:write"},
},
})
ExternalAuth is mutually exclusive with OAuth and OAuth2. Only /.well-known/oauth-protected-resource is mounted (RFC 9728), advertising AuthorizationServers; no local /authorize, /token, or /register endpoints are exposed. Unauthenticated or invalid requests get a 401 with a WWW-Authenticate: Bearer resource_metadata="..." header pointing MCP clients at that metadata.
A plain function can be adapted to the interface with oauth2.TokenVerifierFunc:
verifier := oauth2.TokenVerifierFunc(func(ctx context.Context, token string) (*oauth2.TokenInfo, error) {
claims, err := verifyJWT(token) // your JWKS-backed verification
if err != nil {
return nil, err
}
return &oauth2.TokenInfo{
Subject: claims.Subject,
Scope: claims.Scope,
Actor: claims.ActorChain, // RFC 8693 "act" delegation chain, outermost first
Claims: claims.Raw,
}, nil
})
TokenInfo.Actor and TokenInfo.Claims are only populated for externally-verified tokens; read the delegation chain from a request with oauth2.GetActorFromContext(ctx). Use Claims for custom policy decisions, e.g. with ToolAuthorizer (see the server middleware guide).
Security Notes¶
- HTTPS Required - Always use HTTPS in production
- Strong Secrets - Use cryptographically random client secrets
- Token Expiration - Set appropriate token lifetime
- PKCE Required - Never disable PKCE
- Validate Redirect URIs - Strictly validate registered redirect URIs