auth: add OAuth 2.0 Protected Resource Metadata handler

This change adds support for RFC 9728 (OAuth 2.0 Protected Resource Metadata)
by introducing a `ProtectedResourceMetadataHandler`.

The handler includes built-in CORS support with `Access-Control-Allow-Origin: *`
by default, as OAuth metadata is public information meant for client discovery.

Documentation includes examples for using custom CORS policies with
popular middleware libraries (github.com/rs/cors and github.com/jub0bs/cors).

The implementation follows RFC 9728 §3.1 for OAuth 2.0 Authorization Server
Metadata discovery, enabling clients to discover protected resource capabilities
and authentication requirements.
This commit is contained in:
Eliya Sadan 2025-12-08 21:54:52 +02:00 • committed by GitHub
commit d6d6edd6d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 275 additions and 11 deletions

View file

@ -44,6 +44,7 @@ go test -cover
### Public Endpoints (No Authentication Required)
- `GET /health` - Health check
- `GET /.well-known/oauth-protected-resource` - OAuth 2.0 Protected Resource Metadata (RFC 9728)
### MCP Endpoints (Authentication Required)
@ -228,6 +229,41 @@ authenticatedHandler := authMiddleware(customMiddleware(mcpHandler))
4. **Token Expiration**: Set appropriate token expiration times
5. **Principle of Least Privilege**: Grant only the minimum required scopes
## CORS Support
The OAuth metadata endpoint (`/.well-known/oauth-protected-resource`) supports CORS to enable
cross-origin client discovery. It sets `Access-Control-Allow-Origin: *` by default because
OAuth metadata is public information meant for client discovery (RFC 9728 §3.1).
### Custom CORS Policies
For more sophisticated CORS requirements (origin validation, credentials, etc.), wrap the handler
with a CORS middleware library:
**Using github.com/rs/cors:**
```go
import "github.com/rs/cors"
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://example.com"},
AllowedMethods: []string{"GET", "OPTIONS"},
})
http.Handle("/.well-known/oauth-protected-resource",
c.Handler(auth.ProtectedResourceMetadataHandler(metadata)))
```
**Using github.com/jub0bs/cors:**
```go
import "github.com/jub0bs/cors"
corsMiddleware, err := cors.NewMiddleware(cors.Config{
Origins: []string{"https://example.com"},
Methods: []string{"GET", "OPTIONS"},
})
http.Handle("/.well-known/oauth-protected-resource",
corsMiddleware.Wrap(auth.ProtectedResourceMetadataHandler(metadata)))
```
## Use Cases
**Ideal for:**

View file

@ -10,6 +10,7 @@ require (
require (
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
)
replace github.com/modelcontextprotocol/go-sdk => ../../../

View file

@ -6,5 +6,7 @@ github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIy
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=

View file

@ -20,6 +20,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/modelcontextprotocol/go-sdk/auth"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/go-sdk/oauthex"
)
// This example demonstrates how to integrate auth.RequireBearerToken middleware
@ -237,7 +238,8 @@ func main() {
// Create authentication middleware.
jwtAuth := auth.RequireBearerToken(verifyJWT, &auth.RequireBearerTokenOptions{
Scopes: []string{"read"}, // Require "read" permission
Scopes: []string{"read"}, // Require "read" permission
ResourceMetadataURL: "http://localhost:8080/.well-known/oauth-protected-resource",
})
apiKeyAuth := auth.RequireBearerToken(verifyAPIKey, &auth.RequireBearerTokenOptions{
@ -340,22 +342,36 @@ func main() {
})
})
// OAuth protected resource metadata endpoint.
// This endpoint provides OAuth configuration information to clients.
// CORS is enabled by default to support cross-origin client discovery.
metadata := &oauthex.ProtectedResourceMetadata{
Resource: "http://localhost:8080/mcp/jwt",
AuthorizationServers: []string{
"https://auth.example.com/.well-known/openid-configuration",
},
ScopesSupported: []string{"read", "write"},
}
http.Handle("/.well-known/oauth-protected-resource",
auth.ProtectedResourceMetadataHandler(metadata))
// Start the HTTP server.
log.Println("Authenticated MCP Server")
log.Println("========================")
log.Println("Server starting on", *httpAddr)
log.Println()
log.Println("Available endpoints:")
log.Println(" GET /health - Health check (no auth)")
log.Println(" GET /generate-token - Generate JWT token")
log.Println(" POST /generate-api-key - Generate API key")
log.Println(" POST /mcp/jwt - MCP endpoint (JWT auth)")
log.Println(" POST /mcp/apikey - MCP endpoint (API key auth)")
log.Println(" GET /health - Health check (no auth)")
log.Println(" GET /.well-known/oauth-protected-resource - OAuth resource metadata")
log.Println(" GET /generate-token - Generate JWT token")
log.Println(" POST /generate-api-key - Generate API key")
log.Println(" POST /mcp/jwt - MCP endpoint (JWT auth)")
log.Println(" POST /mcp/apikey - MCP endpoint (API key auth)")
log.Println()
log.Println("Available MCP Tools:")
log.Println(" - say_hi - Simple greeting (any auth)")
log.Println(" - get_user_info - Get user info (read scope)")
log.Println(" - create_resource - Create resource (write scope)")
log.Println(" - say_hi - Simple greeting (any auth)")
log.Println(" - get_user_info - Get user info (read scope)")
log.Println(" - create_resource - Create resource (write scope)")
log.Println()
log.Println("Example usage:")
log.Println(" # Generate a token")