Add OpenAPI support and enhance configuration handling

- Integrated OpenAPI loading functionality into the engine's Load and Reload processes, allowing for better API management.
- Updated the OpenAPI configuration to set a default BaseURL and ensure it does not have a trailing slash.
- Implemented the Attach method to connect the OpenAPI server to the Gin router, facilitating API endpoint management.
- Removed the obsolete hello package to streamline the OpenAPI module.
This commit is contained in:
Max 2025-07-20 18:52:30 +08:00
parent 0fff602c93
commit b96a400869
9 changed files with 348 additions and 1 deletions

View file

@ -22,6 +22,7 @@ import (
"github.com/yaoapp/yao/moapi"
"github.com/yaoapp/yao/model"
"github.com/yaoapp/yao/neo"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/pack"
"github.com/yaoapp/yao/pipe"
"github.com/yaoapp/yao/plugin"
@ -279,6 +280,13 @@ func Load(cfg config.Config, options LoadOption) (warnings []Warning, err error)
}
}
// Load OpenAPI
_, err = openapi.Load(cfg)
if err != nil {
// printErr(cfg.Mode, "OpenAPI", err)
warnings = append(warnings, Warning{Widget: "OpenAPI", Error: err})
}
// Execute AfterLoad Process if exists
if share.App.AfterLoad != "" && !options.IgnoredAfterLoad {
p, err := process.Of(share.App.AfterLoad, options)
@ -468,6 +476,12 @@ func Reload(cfg config.Config, options LoadOption) (err error) {
printErr(cfg.Mode, "Neo", err)
}
// Load OpenAPI
_, err = openapi.Load(cfg)
if err != nil {
printErr(cfg.Mode, "OpenAPI", err)
}
// Execute AfterLoad Process if exists
if share.App.AfterLoad != "" && !options.IgnoredAfterLoad {
options.IsReload = true

View file

@ -249,6 +249,13 @@ func (config *Config) UnmarshalJSON(data []byte) error {
}
// Set defaults if needed
if config.BaseURL == "" {
config.BaseURL = "/v1"
}
// Format the BaseURL should not have trailing slash
config.BaseURL = strings.TrimSuffix(config.BaseURL, "/")
if config.Cache == "" {
config.Cache = "__yao.oauth.cache"
}

140
openapi/docs/oauth.md Normal file
View file

@ -0,0 +1,140 @@
# OAuth 2.0/2.1 Route Mapping (RFC Standards + MCP Protocol Support)
## OAuth Core Endpoints (OAuth 2.1 Required)
| Endpoint | HTTP Method | Purpose | RFC Standard | MCP Requirement |
| ------------------- | ----------- | ------------------------------------------------ | ----------------------- | --------------------- |
| `/oauth/authorize` | GET, POST | Authorization request, obtain authorization code | RFC 6749 Section 3.1 | ✅ Required |
| `/oauth/token` | POST | Token request, exchange for access token | RFC 6749 Section 3.2 | ✅ Required |
| `/oauth/revoke` | POST | Revoke access token or refresh token | RFC 7009 | ✅ Required |
| `/oauth/introspect` | POST | Check token status and metadata | RFC 7662 | ✅ Recommended |
| `/oauth/jwks` | GET | JSON Web Key Set for token verification | RFC 7517 | ✅ Required (for JWT) |
| `/oauth/userinfo` | GET, POST | Retrieve user information | OpenID Connect Core 1.0 | ✅ Recommended |
## OAuth Extended Endpoints
| Endpoint | HTTP Method | Purpose | RFC Standard | MCP Requirement |
| ----------------------------- | ----------- | ----------------------------- | ------------ | --------------- |
| `/oauth/register` | POST | Dynamic client registration | RFC 7591 | ✅ Required |
| `/oauth/register/:client_id` | GET | Retrieve client configuration | RFC 7592 | ✅ Optional |
| `/oauth/register/:client_id` | PUT | Update client configuration | RFC 7592 | ✅ Optional |
| `/oauth/register/:client_id` | DELETE | Delete client configuration | RFC 7592 | ✅ Optional |
| `/oauth/device_authorization` | POST | Device authorization flow | RFC 8628 | ✅ Optional |
| `/oauth/par` | POST | Pushed Authorization Request | RFC 9126 | ✅ Recommended |
| `/oauth/token_exchange` | POST | Token exchange | RFC 8693 | ✅ Optional |
## Discovery and Metadata Endpoints
| Endpoint | HTTP Method | Purpose | RFC Standard | MCP Requirement |
| ----------------------------------------- | ----------- | ----------------------------- | ---------------------------- | --------------- |
| `/.well-known/oauth-authorization-server` | GET | Authorization server metadata | RFC 8414 | ✅ Required |
| `/.well-known/openid_configuration` | GET | OpenID Connect configuration | OpenID Connect Discovery 1.0 | ✅ Optional |
| `/.well-known/oauth-protected-resource` | GET | Protected resource metadata | RFC 9728 | ✅ Required |
## Interface Method Mapping
Each route handler corresponds to interface methods:
- `oauthAuthorize``OAuth.Authorize()`
- `oauthToken``OAuth.Token()`, `OAuth.RefreshToken()`
- `oauthRevoke``OAuth.Revoke()`
- `oauthIntrospect``OAuth.Introspect()`
- `oauthJWKS``OAuth.JWKS()`
- `oauthUserInfo``OAuth.UserInfo()`
- `oauthRegister``OAuth.Register()`, `OAuth.DynamicClientRegistration()`
- `oauthGetClient` → Client query methods
- `oauthUpdateClient``OAuth.UpdateClient()`
- `oauthDeleteClient``OAuth.DeleteClient()`
- `oauthDeviceAuthorization``OAuth.DeviceAuthorization()`
- `oauthPushedAuthorizationRequest``OAuth.PushAuthorizationRequest()`
- `oauthTokenExchange``OAuth.TokenExchange()`
- `oauthServerMetadata``OAuth.GetServerMetadata()`
- `oauthProtectedResourceMetadata``OAuth.GetProtectedResourceMetadata()`
## MCP Protocol Special Requirements
1. **Resource Parameter Validation**: Using `OAuth.ValidateResourceParameter()`
2. **Canonical Resource URI**: Using `OAuth.GetCanonicalResourceURI()`
3. **State Parameter Security**: Using `OAuth.ValidateStateParameter()`, `OAuth.GenerateStateParameter()`
4. **Redirect URI Validation**: Using `OAuth.ValidateRedirectURI()`
5. **Token Binding**: Using `OAuth.ValidateTokenBinding()`
6. **Refresh Token Rotation**: Using `OAuth.RotateRefreshToken()`
## Security Considerations
- All POST endpoints should validate CSRF protection
- `/oauth/authorize` supports both GET and POST, but POST is recommended for enhanced security
- PKCE (Proof Key for Code Exchange) should be enforced in all authorization code flows
- All endpoints should support HTTPS
- Token endpoints require client authentication
- State parameters are required in authorization flows
## Typical Flows
1. **Authorization Code Flow**: `/oauth/authorize``/oauth/token`
2. **Refresh Token**: `/oauth/token` (grant_type=refresh_token)
3. **Token Revocation**: `/oauth/revoke`
4. **Device Flow**: `/oauth/device_authorization``/oauth/token`
5. **Token Introspection**: `/oauth/introspect`
6. **Dynamic Registration**: `/oauth/register`
## MCP Authorization Flow Overview
The Model Context Protocol requires specific OAuth 2.1 implementation patterns:
### Authorization Server Discovery
1. **Protected Resource Metadata**: MCP servers MUST implement RFC 9728
2. **WWW-Authenticate Header**: Used in 401 responses to indicate authorization server location
3. **Server Metadata**: Authorization servers MUST provide RFC 8414 metadata
### Resource Parameter Implementation
MCP clients MUST implement Resource Indicators (RFC 8707):
```
&resource=https%3A%2F%2Fmcp.example.com
```
- MUST be included in both authorization and token requests
- MUST identify the target MCP server
- MUST use canonical URI format
### Canonical Server URI Examples
**Valid canonical URIs:**
- `https://mcp.example.com/mcp`
- `https://mcp.example.com`
- `https://mcp.example.com:8443`
- `https://mcp.example.com/server/mcp`
**Invalid canonical URIs:**
- `mcp.example.com` (missing scheme)
- `https://mcp.example.com#fragment` (contains fragment)
### Access Token Usage
- MUST use Authorization header: `Authorization: Bearer <access-token>`
- MUST NOT include tokens in URI query strings
- MUST validate token audience binding
- MUST implement token theft protection
### Dynamic Client Registration
Authorization servers SHOULD support RFC 7591 for seamless client onboarding:
- Enables automatic registration with new authorization servers
- Reduces user friction
- Allows authorization servers to implement custom registration policies
### Security Requirements
1. **Token Audience Binding**: Tokens MUST be bound to intended audiences
2. **Communication Security**: All endpoints MUST use HTTPS
3. **PKCE Protection**: MUST implement PKCE for authorization code flows
4. **Open Redirection Prevention**: MUST validate redirect URIs exactly
5. **Refresh Token Rotation**: MUST rotate refresh tokens for public clients
This route planning follows OAuth 2.1 best practices, supports all MCP protocol requirements, and provides complete OAuth authorization server functionality with enhanced security measures specifically designed for Model Context Protocol implementations.

35
openapi/hello.go Normal file
View file

@ -0,0 +1,35 @@
package openapi
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/share"
)
// attachHelloWorld attaches the hello world handlers to the router
func (openapi *OpenAPI) attachHelloWorld(base *gin.RouterGroup) {
// hello handlers
hello := base.Group("/helloworld")
// Health check
hello.GET("/hello", openapi.helloWorldHello)
hello.POST("/hello", openapi.helloWorldHello)
}
// helloWorldHello is the handler for the hello world endpoint
func (openapi *OpenAPI) helloWorldHello(c *gin.Context) {
serverTime := time.Now().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"MESSAGE": "HELLO, WORLD",
"SERVER_TIME": serverTime,
"VERSION": share.VERSION,
"PRVERSION": share.PRVERSION,
"CUI": share.CUI,
"PRCUI": share.PRCUI,
"APP": share.App.Name,
"APP_VERSION": share.App.Version,
})
}

View file

@ -1 +0,0 @@
package hello

115
openapi/oauth.go Normal file
View file

@ -0,0 +1,115 @@
package openapi
import "github.com/gin-gonic/gin"
// OAuth handlers
func (openapi *OpenAPI) attachOAuth(base *gin.RouterGroup) {
// OAuth Core Endpoints (RFC 6749, OAuth 2.1)
oauth := base.Group("/oauth")
// Authorization endpoint - RFC 6749 Section 3.1
oauth.GET("/authorize", openapi.oauthAuthorize)
oauth.POST("/authorize", openapi.oauthAuthorize) // Support both GET and POST
// Token endpoint - RFC 6749 Section 3.2
oauth.POST("/token", openapi.oauthToken)
// Token revocation endpoint - RFC 7009
oauth.POST("/revoke", openapi.oauthRevoke)
// Token introspection endpoint - RFC 7662
oauth.POST("/introspect", openapi.oauthIntrospect)
// JSON Web Key Set endpoint - RFC 7517
oauth.GET("/jwks", openapi.oauthJWKS)
// UserInfo endpoint - OpenID Connect Core 1.0
oauth.GET("/userinfo", openapi.oauthUserInfo)
oauth.POST("/userinfo", openapi.oauthUserInfo) // Support both GET and POST
// OAuth Extended Endpoints
// Dynamic Client Registration - RFC 7591 (Required by MCP)
oauth.POST("/register", openapi.oauthRegister)
// Client Configuration - RFC 7592
oauth.GET("/register/:client_id", openapi.oauthGetClient)
oauth.PUT("/register/:client_id", openapi.oauthUpdateClient)
oauth.DELETE("/register/:client_id", openapi.oauthDeleteClient)
// Device Authorization Flow - RFC 8628
oauth.POST("/device_authorization", openapi.oauthDeviceAuthorization)
// Pushed Authorization Request - RFC 9126
oauth.POST("/par", openapi.oauthPushedAuthorizationRequest)
// Token Exchange - RFC 8693
oauth.POST("/token_exchange", openapi.oauthTokenExchange)
// OAuth Discovery and Metadata Endpoints
wellKnown := base.Group("/.well-known")
// OAuth Authorization Server Metadata - RFC 8414 (Required by MCP)
wellKnown.GET("/oauth-authorization-server", openapi.oauthServerMetadata)
// OpenID Connect Discovery - OpenID Connect Discovery 1.0
wellKnown.GET("/openid_configuration", openapi.oauthOpenIDConfiguration)
// OAuth Protected Resource Metadata - RFC 9728 (Required by MCP)
wellKnown.GET("/oauth-protected-resource", openapi.oauthProtectedResourceMetadata)
}
// OAuth Core Endpoints Implementation
// oauthAuthorize handles authorization requests - RFC 6749 Section 3.1
func (openapi *OpenAPI) oauthAuthorize(c *gin.Context) {}
// oauthToken handles token requests - RFC 6749 Section 3.2
func (openapi *OpenAPI) oauthToken(c *gin.Context) {}
// oauthRevoke handles token revocation - RFC 7009
func (openapi *OpenAPI) oauthRevoke(c *gin.Context) {}
// oauthIntrospect handles token introspection - RFC 7662
func (openapi *OpenAPI) oauthIntrospect(c *gin.Context) {}
// oauthJWKS returns JSON Web Key Set - RFC 7517
func (openapi *OpenAPI) oauthJWKS(c *gin.Context) {}
// oauthUserInfo returns user information - OpenID Connect Core 1.0
func (openapi *OpenAPI) oauthUserInfo(c *gin.Context) {}
// OAuth Extended Endpoints Implementation
// oauthRegister handles dynamic client registration - RFC 7591
func (openapi *OpenAPI) oauthRegister(c *gin.Context) {}
// oauthGetClient retrieves client configuration - RFC 7592
func (openapi *OpenAPI) oauthGetClient(c *gin.Context) {}
// oauthUpdateClient updates client configuration - RFC 7592
func (openapi *OpenAPI) oauthUpdateClient(c *gin.Context) {}
// oauthDeleteClient deletes client configuration - RFC 7592
func (openapi *OpenAPI) oauthDeleteClient(c *gin.Context) {}
// oauthDeviceAuthorization handles device authorization - RFC 8628
func (openapi *OpenAPI) oauthDeviceAuthorization(c *gin.Context) {}
// oauthPushedAuthorizationRequest handles PAR - RFC 9126
func (openapi *OpenAPI) oauthPushedAuthorizationRequest(c *gin.Context) {}
// oauthTokenExchange handles token exchange - RFC 8693
func (openapi *OpenAPI) oauthTokenExchange(c *gin.Context) {}
// OAuth Discovery and Metadata Endpoints Implementation
// oauthServerMetadata returns authorization server metadata - RFC 8414
func (openapi *OpenAPI) oauthServerMetadata(c *gin.Context) {}
// oauthOpenIDConfiguration returns OpenID Connect configuration
func (openapi *OpenAPI) oauthOpenIDConfiguration(c *gin.Context) {}
// oauthProtectedResourceMetadata returns protected resource metadata - RFC 9728
func (openapi *OpenAPI) oauthProtectedResourceMetadata(c *gin.Context) {}

View file

@ -3,6 +3,7 @@ package openapi
import (
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/openapi/oauth"
@ -50,3 +51,24 @@ func Load(appConfig config.Config) (*OpenAPI, error) {
Server = &OpenAPI{Config: &config, OAuth: oauthService}
return Server, nil
}
// Attach attaches the OpenAPI server to the router
func (openapi *OpenAPI) Attach(router *gin.Engine) {
// Ignore if the OpenAPI server is not configured
if openapi.Config == nil {
return
}
// Basic Groups
baseURL := openapi.Config.BaseURL
group := router.Group(baseURL)
// OAuth handlers
openapi.attachOAuth(group)
// Hello World handlers
openapi.attachHelloWorld(group)
// Custom handlers (Defined by developer)
}

View file

@ -10,6 +10,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/share"
"github.com/yaoapp/yao/sui/api"
)
@ -23,6 +24,14 @@ var Middlewares = []gin.HandlerFunc{
// withStaticFileServer static file server
func withStaticFileServer(c *gin.Context) {
// Handle OpenAPI server
if openapi.Server != nil && openapi.Server.Config != nil && openapi.Server.Config.BaseURL != "" {
if strings.HasPrefix(c.Request.URL.Path, openapi.Server.Config.BaseURL+"/") {
c.Next()
return
}
}
// Handle API & websocket
length := len(c.Request.URL.Path)
if (length >= 5 && c.Request.URL.Path[0:5] == "/api/") ||

View file

@ -8,6 +8,7 @@ import (
"github.com/yaoapp/gou/server/http"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/neo"
"github.com/yaoapp/yao/openapi"
"github.com/yaoapp/yao/share"
)
@ -40,6 +41,11 @@ func Start(cfg config.Config) (*http.Server, error) {
neo.Neo.API(router, "/api/__yao/neo")
}
// OpenAPI Server
if openapi.Server != nil {
openapi.Server.Attach(router)
}
go func() {
err = srv.Start()
}()