feat: implement RFC 9728 OAuth 2.0 Protected Resource Metadata for MCP server discovery (#5092)

This commit is contained in:
Yang Luo
2026-02-15 16:40:48 +08:00
parent f966f4a0f9
commit 7814caf2ab
4 changed files with 117 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"github.com/casdoor/casdoor/object"
)
// GetOauthProtectedResourceMetadata
// @Title GetOauthProtectedResourceMetadata
// @Tag OAuth 2.0 API
// @Description Get OAuth 2.0 Protected Resource Metadata (RFC 9728)
// @Success 200 {object} object.OauthProtectedResourceMetadata
// @router /.well-known/oauth-protected-resource [get]
func (c *RootController) GetOauthProtectedResourceMetadata() {
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOauthProtectedResourceMetadata(host)
c.ServeJSON()
}
// GetOauthProtectedResourceMetadataByApplication
// @Title GetOauthProtectedResourceMetadataByApplication
// @Tag OAuth 2.0 API
// @Description Get OAuth 2.0 Protected Resource Metadata for specific application (RFC 9728)
// @Param application path string true "application name"
// @Success 200 {object} object.OauthProtectedResourceMetadata
// @router /.well-known/:application/oauth-protected-resource [get]
func (c *RootController) GetOauthProtectedResourceMetadataByApplication() {
application := c.Ctx.Input.Param(":application")
host := c.Ctx.Request.Host
c.Data["json"] = object.GetOauthProtectedResourceMetadataByApplication(host, application)
c.ServeJSON()
}

View File

@@ -0,0 +1,59 @@
// Copyright 2026 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
)
// OauthProtectedResourceMetadata represents RFC 9728 OAuth 2.0 Protected Resource Metadata
type OauthProtectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
ResourceSigningAlg []string `json:"resource_signing_alg_values_supported,omitempty"`
ResourceDocumentation string `json:"resource_documentation,omitempty"`
}
// GetOauthProtectedResourceMetadata returns RFC 9728 Protected Resource Metadata for global discovery
func GetOauthProtectedResourceMetadata(host string) OauthProtectedResourceMetadata {
_, originBackend := getOriginFromHost(host)
return OauthProtectedResourceMetadata{
Resource: originBackend,
AuthorizationServers: []string{originBackend},
BearerMethodsSupported: []string{"header"},
ScopesSupported: []string{"openid", "profile", "email", "read", "write"},
ResourceSigningAlg: []string{"RS256"},
}
}
// GetOauthProtectedResourceMetadataByApplication returns RFC 9728 Protected Resource Metadata for application-specific discovery
func GetOauthProtectedResourceMetadataByApplication(host string, applicationName string) OauthProtectedResourceMetadata {
_, originBackend := getOriginFromHost(host)
// For application-specific discovery, the resource identifier includes the application name
resourceIdentifier := fmt.Sprintf("%s/.well-known/%s", originBackend, applicationName)
authServer := fmt.Sprintf("%s/.well-known/%s", originBackend, applicationName)
return OauthProtectedResourceMetadata{
Resource: resourceIdentifier,
AuthorizationServers: []string{authServer},
BearerMethodsSupported: []string{"header"},
ScopesSupported: []string{"openid", "profile", "email", "read", "write"},
ResourceSigningAlg: []string{"RS256"},
}
}

View File

@@ -94,6 +94,17 @@ func denyMcpRequest(ctx *context.Context) {
Data: T(ctx, "auth:Unauthorized operation"),
})
// Add WWW-Authenticate header per MCP Authorization spec (RFC 9728)
// Use the same logic as getOriginFromHost to determine the scheme
host := ctx.Request.Host
scheme := "https"
if !strings.Contains(host, ".") {
// localhost:8000 or computer-name:80
scheme = "http"
}
resourceMetadataUrl := fmt.Sprintf("%s://%s/.well-known/oauth-protected-resource", scheme, host)
ctx.Output.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"casdoor\", resource_metadata=\"%s\"", resourceMetadataUrl))
ctx.Output.SetStatus(http.StatusUnauthorized)
_ = ctx.Output.JSON(resp, true, false)
}

View File

@@ -324,6 +324,8 @@ func InitAPI() {
web.Router("/.well-known/:application/jwks", &controllers.RootController{}, "*:GetJwksByApplication")
web.Router("/.well-known/webfinger", &controllers.RootController{}, "GET:GetWebFinger")
web.Router("/.well-known/:application/webfinger", &controllers.RootController{}, "GET:GetWebFingerByApplication")
web.Router("/.well-known/oauth-protected-resource", &controllers.RootController{}, "GET:GetOauthProtectedResourceMetadata")
web.Router("/.well-known/:application/oauth-protected-resource", &controllers.RootController{}, "GET:GetOauthProtectedResourceMetadataByApplication")
web.Router("/cas/:organization/:application/serviceValidate", &controllers.RootController{}, "GET:CasServiceValidate")
web.Router("/cas/:organization/:application/proxyValidate", &controllers.RootController{}, "GET:CasProxyValidate")