mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 09:56:31 +02:00
mcp: add oauth metadata endpoint (#5579)
This commit is contained in:
parent
2e7d1c7f12
commit
cb0e8aaf06
10 changed files with 324 additions and 32 deletions
|
@ -20,6 +20,7 @@ func (b *Builder) buildVirtualHost(
|
||||||
options *config.Options,
|
options *config.Options,
|
||||||
name string,
|
name string,
|
||||||
host string,
|
host string,
|
||||||
|
hasMCPPolicy bool,
|
||||||
) (*envoy_config_route_v3.VirtualHost, error) {
|
) (*envoy_config_route_v3.VirtualHost, error) {
|
||||||
vh := &envoy_config_route_v3.VirtualHost{
|
vh := &envoy_config_route_v3.VirtualHost{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -36,7 +37,7 @@ func (b *Builder) buildVirtualHost(
|
||||||
}
|
}
|
||||||
|
|
||||||
// these routes match /.pomerium/... and similar paths
|
// these routes match /.pomerium/... and similar paths
|
||||||
rs, err := b.buildPomeriumHTTPRoutes(options, host)
|
rs, err := b.buildPomeriumHTTPRoutes(options, host, hasMCPPolicy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,14 +50,14 @@ func (b *Builder) buildMainRouteConfiguration(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
allHosts, err := getAllRouteableHosts(cfg.Options, cfg.Options.Addr)
|
allHosts, mcpHosts, err := getAllRouteableHosts(cfg.Options, cfg.Options.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var virtualHosts []*envoy_config_route_v3.VirtualHost
|
var virtualHosts []*envoy_config_route_v3.VirtualHost
|
||||||
for _, host := range allHosts {
|
for _, host := range allHosts {
|
||||||
vh, err := b.buildVirtualHost(cfg.Options, host, host)
|
vh, err := b.buildVirtualHost(cfg.Options, host, host, mcpHosts[host])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ func (b *Builder) buildMainRouteConfiguration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vh, err := b.buildVirtualHost(cfg.Options, "catch-all", "*")
|
vh, err := b.buildVirtualHost(cfg.Options, "catch-all", "*", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -106,21 +106,28 @@ func (b *Builder) buildMainRouteConfiguration(
|
||||||
return rc, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllRouteableHosts(options *config.Options, addr string) ([]string, error) {
|
func getAllRouteableHosts(options *config.Options, addr string) ([]string, map[string]bool, error) {
|
||||||
allHosts := set.NewTreeSet(cmp.Compare[string])
|
allHosts := set.NewTreeSet(cmp.Compare[string])
|
||||||
|
mcpHosts := make(map[string]bool)
|
||||||
|
|
||||||
if addr == options.Addr {
|
if addr == options.Addr {
|
||||||
hosts, err := options.GetAllRouteableHTTPHosts()
|
hosts, hostsMCP, err := options.GetAllRouteableHTTPHosts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
allHosts.InsertSlice(hosts)
|
allHosts.InsertSlice(hosts)
|
||||||
|
// Merge any MCP hosts
|
||||||
|
for host, isMCP := range hostsMCP {
|
||||||
|
if isMCP {
|
||||||
|
mcpHosts[host] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr == options.GetGRPCAddr() {
|
if addr == options.GetGRPCAddr() {
|
||||||
hosts, err := options.GetAllRouteableGRPCHosts()
|
hosts, err := options.GetAllRouteableGRPCHosts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
allHosts.InsertSlice(hosts)
|
allHosts.InsertSlice(hosts)
|
||||||
}
|
}
|
||||||
|
@ -131,7 +138,7 @@ func getAllRouteableHosts(options *config.Options, addr string) ([]string, error
|
||||||
filtered = append(filtered, host)
|
filtered = append(filtered, host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered, nil
|
return filtered, mcpHosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRouteConfiguration(name string, virtualHosts []*envoy_config_route_v3.VirtualHost) *envoy_config_route_v3.RouteConfiguration {
|
func newRouteConfiguration(name string, virtualHosts []*envoy_config_route_v3.VirtualHost) *envoy_config_route_v3.RouteConfiguration {
|
||||||
|
|
|
@ -195,7 +195,7 @@ func Test_getAllDomains(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Run("routable", func(t *testing.T) {
|
t.Run("routable", func(t *testing.T) {
|
||||||
t.Run("http", func(t *testing.T) {
|
t.Run("http", func(t *testing.T) {
|
||||||
actual, err := getAllRouteableHosts(options, "127.0.0.1:9000")
|
actual, _, err := getAllRouteableHosts(options, "127.0.0.1:9000")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect := []string{
|
expect := []string{
|
||||||
"a.example.com",
|
"a.example.com",
|
||||||
|
@ -214,7 +214,7 @@ func Test_getAllDomains(t *testing.T) {
|
||||||
assert.Equal(t, expect, actual)
|
assert.Equal(t, expect, actual)
|
||||||
})
|
})
|
||||||
t.Run("grpc", func(t *testing.T) {
|
t.Run("grpc", func(t *testing.T) {
|
||||||
actual, err := getAllRouteableHosts(options, "127.0.0.1:9001")
|
actual, _, err := getAllRouteableHosts(options, "127.0.0.1:9001")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect := []string{
|
expect := []string{
|
||||||
"authorize.example.com:9001",
|
"authorize.example.com:9001",
|
||||||
|
@ -225,7 +225,7 @@ func Test_getAllDomains(t *testing.T) {
|
||||||
t.Run("both", func(t *testing.T) {
|
t.Run("both", func(t *testing.T) {
|
||||||
newOptions := *options
|
newOptions := *options
|
||||||
newOptions.GRPCAddr = newOptions.Addr
|
newOptions.GRPCAddr = newOptions.Addr
|
||||||
actual, err := getAllRouteableHosts(&newOptions, "127.0.0.1:9000")
|
actual, _, err := getAllRouteableHosts(&newOptions, "127.0.0.1:9000")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
expect := []string{
|
expect := []string{
|
||||||
"a.example.com",
|
"a.example.com",
|
||||||
|
@ -252,7 +252,7 @@ func Test_getAllDomains(t *testing.T) {
|
||||||
options.Policies = []config.Policy{
|
options.Policies = []config.Policy{
|
||||||
{From: "https://a.example.com"},
|
{From: "https://a.example.com"},
|
||||||
}
|
}
|
||||||
actual, err := getAllRouteableHosts(options, ":443")
|
actual, _, err := getAllRouteableHosts(options, ":443")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, []string{"a.example.com"}, actual)
|
assert.Equal(t, []string{"a.example.com"}, actual)
|
||||||
})
|
})
|
||||||
|
|
|
@ -50,6 +50,7 @@ func (b *Builder) buildGRPCRoutes() ([]*envoy_config_route_v3.Route, error) {
|
||||||
func (b *Builder) buildPomeriumHTTPRoutes(
|
func (b *Builder) buildPomeriumHTTPRoutes(
|
||||||
options *config.Options,
|
options *config.Options,
|
||||||
host string,
|
host string,
|
||||||
|
isMCPHost bool,
|
||||||
) ([]*envoy_config_route_v3.Route, error) {
|
) ([]*envoy_config_route_v3.Route, error) {
|
||||||
var routes []*envoy_config_route_v3.Route
|
var routes []*envoy_config_route_v3.Route
|
||||||
|
|
||||||
|
@ -60,6 +61,7 @@ func (b *Builder) buildPomeriumHTTPRoutes(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !isFrontingAuthenticate {
|
if !isFrontingAuthenticate {
|
||||||
|
// Add common routes
|
||||||
routes = append(routes,
|
routes = append(routes,
|
||||||
b.buildControlPlanePathRoute(options, "/ping"),
|
b.buildControlPlanePathRoute(options, "/ping"),
|
||||||
b.buildControlPlanePathRoute(options, "/healthz"),
|
b.buildControlPlanePathRoute(options, "/healthz"),
|
||||||
|
@ -68,6 +70,11 @@ func (b *Builder) buildPomeriumHTTPRoutes(
|
||||||
b.buildControlPlanePathRoute(options, "/.well-known/pomerium"),
|
b.buildControlPlanePathRoute(options, "/.well-known/pomerium"),
|
||||||
b.buildControlPlanePrefixRoute(options, "/.well-known/pomerium/"),
|
b.buildControlPlanePrefixRoute(options, "/.well-known/pomerium/"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Only add oauth-authorization-server route if there's an MCP policy for this host
|
||||||
|
if isMCPHost {
|
||||||
|
routes = append(routes, b.buildControlPlanePathRoute(options, "/.well-known/oauth-authorization-server"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authRoutes, err := b.buildPomeriumAuthenticateHTTPRoutes(options, host)
|
authRoutes, err := b.buildPomeriumAuthenticateHTTPRoutes(options, host)
|
||||||
|
|
|
@ -104,7 +104,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
AuthenticateURLString: "https://authenticate.example.com",
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
AuthenticateCallbackPath: "/oauth2/callback",
|
AuthenticateCallbackPath: "/oauth2/callback",
|
||||||
}
|
}
|
||||||
routes, err := b.buildPomeriumHTTPRoutes(options, "authenticate.example.com")
|
routes, err := b.buildPomeriumHTTPRoutes(options, "authenticate.example.com", false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.AssertProtoJSONEqual(t, `[
|
testutil.AssertProtoJSONEqual(t, `[
|
||||||
|
@ -125,7 +125,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
|
||||||
AuthenticateURLString: "https://authenticate.example.com",
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
AuthenticateCallbackPath: "/oauth2/callback",
|
AuthenticateCallbackPath: "/oauth2/callback",
|
||||||
}
|
}
|
||||||
routes, err := b.buildPomeriumHTTPRoutes(options, "authenticate.example.com")
|
routes, err := b.buildPomeriumHTTPRoutes(options, "authenticate.example.com", false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.AssertProtoJSONEqual(t, "null", routes)
|
testutil.AssertProtoJSONEqual(t, "null", routes)
|
||||||
})
|
})
|
||||||
|
@ -2244,3 +2244,107 @@ func mustParseURL(t *testing.T, str string) *url.URL {
|
||||||
func ptr[T any](v T) *T {
|
func ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_buildPomeriumHTTPRoutesWithMCP(t *testing.T) {
|
||||||
|
routeString := func(typ, name string) string {
|
||||||
|
str := `{
|
||||||
|
"name": "pomerium-` + typ + `-` + name + `",
|
||||||
|
"decorator": {
|
||||||
|
"operation": "internal: ${method} ${host}${path}"
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"` + typ + `": "` + name + `"
|
||||||
|
},
|
||||||
|
"responseHeadersToAdd": [
|
||||||
|
{
|
||||||
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
|
"header": {
|
||||||
|
"key": "X-Frame-Options",
|
||||||
|
"value": "SAMEORIGIN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appendAction": "OVERWRITE_IF_EXISTS_OR_ADD",
|
||||||
|
"header": {
|
||||||
|
"key": "X-XSS-Protection",
|
||||||
|
"value": "1; mode=block"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"cluster": "pomerium-control-plane-http"
|
||||||
|
},
|
||||||
|
"typedPerFilterConfig": {
|
||||||
|
"envoy.filters.http.ext_authz": {
|
||||||
|
"@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute",
|
||||||
|
"checkSettings": {
|
||||||
|
"contextExtensions": {
|
||||||
|
"internal": "true",
|
||||||
|
"route_id": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("without MCP policy", func(t *testing.T) {
|
||||||
|
b := &Builder{filemgr: filemgr.NewManager()}
|
||||||
|
options := &config.Options{
|
||||||
|
Services: "all",
|
||||||
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
|
Policies: []config.Policy{
|
||||||
|
{
|
||||||
|
From: "https://example.com",
|
||||||
|
To: mustParseWeightedURLs(t, "https://to.example.com"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := b.buildPomeriumHTTPRoutes(options, "example.com", false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
hasOAuthServer := false
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.GetMatch().GetPath() == "/.well-known/oauth-authorization-server" {
|
||||||
|
hasOAuthServer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, hasOAuthServer, "/.well-known/oauth-authorization-server route should NOT be present")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with MCP policy", func(t *testing.T) {
|
||||||
|
b := &Builder{filemgr: filemgr.NewManager()}
|
||||||
|
options := &config.Options{
|
||||||
|
Services: "all",
|
||||||
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
|
Policies: []config.Policy{
|
||||||
|
{
|
||||||
|
From: "https://example.com",
|
||||||
|
To: mustParseWeightedURLs(t, "https://to.example.com"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
From: "https://mcp.example.com",
|
||||||
|
To: mustParseWeightedURLs(t, "https://mcp-backend.example.com"),
|
||||||
|
MCP: &config.MCP{}, // This marks the policy as an MCP policy
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err := b.buildPomeriumHTTPRoutes(options, "example.com", true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the expected route structures
|
||||||
|
testutil.AssertProtoJSONEqual(t, `[
|
||||||
|
`+routeString("path", "/ping")+`,
|
||||||
|
`+routeString("path", "/healthz")+`,
|
||||||
|
`+routeString("path", "/.pomerium")+`,
|
||||||
|
`+routeString("prefix", "/.pomerium/")+`,
|
||||||
|
`+routeString("path", "/.well-known/pomerium")+`,
|
||||||
|
`+routeString("prefix", "/.well-known/pomerium/")+`,
|
||||||
|
`+routeString("path", "/.well-known/oauth-authorization-server")+`
|
||||||
|
]`, routes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1273,23 +1273,27 @@ func (o *Options) GetAllRouteableGRPCHosts() ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllRouteableHTTPHosts returns all the possible HTTP hosts handled by the Pomerium options.
|
// GetAllRouteableHTTPHosts returns all the possible HTTP hosts handled by the Pomerium options.
|
||||||
func (o *Options) GetAllRouteableHTTPHosts() ([]string, error) {
|
func (o *Options) GetAllRouteableHTTPHosts() ([]string, map[string]bool, error) {
|
||||||
hosts := goset.NewTreeSet(cmp.Compare[string])
|
hosts := goset.NewTreeSet(cmp.Compare[string])
|
||||||
|
mcpHosts := make(map[string]bool)
|
||||||
|
|
||||||
if IsAuthenticate(o.Services) {
|
if IsAuthenticate(o.Services) {
|
||||||
if o.AuthenticateInternalURLString != "" {
|
if o.AuthenticateInternalURLString != "" {
|
||||||
authenticateURL, err := o.GetInternalAuthenticateURL()
|
authenticateURL, err := o.GetInternalAuthenticateURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
hosts.InsertSlice(urlutil.GetDomainsForURL(authenticateURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort)))
|
domains := urlutil.GetDomainsForURL(authenticateURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort))
|
||||||
|
hosts.InsertSlice(domains)
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.AuthenticateURLString != "" {
|
if o.AuthenticateURLString != "" {
|
||||||
authenticateURL, err := o.GetAuthenticateURL()
|
authenticateURL, err := o.GetAuthenticateURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
hosts.InsertSlice(urlutil.GetDomainsForURL(authenticateURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort)))
|
domains := urlutil.GetDomainsForURL(authenticateURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort))
|
||||||
|
hosts.InsertSlice(domains)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1298,18 +1302,35 @@ func (o *Options) GetAllRouteableHTTPHosts() ([]string, error) {
|
||||||
for policy := range o.GetAllPolicies() {
|
for policy := range o.GetAllPolicies() {
|
||||||
fromURL, err := urlutil.ParseAndValidateURL(policy.From)
|
fromURL, err := urlutil.ParseAndValidateURL(policy.From)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
domains := urlutil.GetDomainsForURL(fromURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort))
|
||||||
|
hosts.InsertSlice(domains)
|
||||||
|
|
||||||
|
// Track if the domains are associated with an MCP policy
|
||||||
|
if policy.IsMCP() {
|
||||||
|
for _, domain := range domains {
|
||||||
|
mcpHosts[domain] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts.InsertSlice(urlutil.GetDomainsForURL(fromURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort)))
|
|
||||||
if policy.TLSDownstreamServerName != "" {
|
if policy.TLSDownstreamServerName != "" {
|
||||||
tlsURL := fromURL.ResolveReference(&url.URL{Host: policy.TLSDownstreamServerName})
|
tlsURL := fromURL.ResolveReference(&url.URL{Host: policy.TLSDownstreamServerName})
|
||||||
hosts.InsertSlice(urlutil.GetDomainsForURL(tlsURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort)))
|
tlsDomains := urlutil.GetDomainsForURL(tlsURL, !o.IsRuntimeFlagSet(RuntimeFlagMatchAnyIncomingPort))
|
||||||
|
hosts.InsertSlice(tlsDomains)
|
||||||
|
|
||||||
|
// Track if the TLS domains are associated with an MCP policy
|
||||||
|
if policy.IsMCP() {
|
||||||
|
for _, domain := range tlsDomains {
|
||||||
|
mcpHosts[domain] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hosts.Slice(), nil
|
return hosts.Slice(), mcpHosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientSecret gets the client secret.
|
// GetClientSecret gets the client secret.
|
||||||
|
|
|
@ -888,22 +888,26 @@ func TestOptions_GetAllRouteableGRPCHosts(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptions_GetAllRouteableHTTPHosts(t *testing.T) {
|
func TestOptions_GetAllRouteableHTTPHosts(t *testing.T) {
|
||||||
p1 := Policy{From: "https://from1.example.com"}
|
to := WeightedURLs{{URL: url.URL{Scheme: "https", Host: "to.example.com"}}}
|
||||||
p1.Validate()
|
p1 := Policy{From: "https://from1.example.com", To: to}
|
||||||
p2 := Policy{From: "https://from2.example.com"}
|
assert.NoError(t, p1.Validate())
|
||||||
p2.Validate()
|
p2 := Policy{From: "https://from2.example.com", To: to}
|
||||||
p3 := Policy{From: "https://from3.example.com", TLSDownstreamServerName: "from.example.com"}
|
assert.NoError(t, p2.Validate())
|
||||||
p3.Validate()
|
p3 := Policy{From: "https://from3.example.com", TLSDownstreamServerName: "from.example.com", To: to}
|
||||||
|
assert.NoError(t, p3.Validate())
|
||||||
|
p4 := Policy{From: "https://from4.example.com", MCP: &MCP{}, To: to}
|
||||||
|
assert.NoError(t, p4.Validate())
|
||||||
|
|
||||||
opts := &Options{
|
opts := &Options{
|
||||||
AuthenticateURLString: "https://authenticate.example.com",
|
AuthenticateURLString: "https://authenticate.example.com",
|
||||||
AuthorizeURLString: "https://authorize.example.com",
|
AuthorizeURLString: "https://authorize.example.com",
|
||||||
DataBrokerURLString: "https://databroker.example.com",
|
DataBrokerURLString: "https://databroker.example.com",
|
||||||
Policies: []Policy{p1, p2, p3},
|
Policies: []Policy{p1, p2, p3, p4},
|
||||||
Services: "all",
|
Services: "all",
|
||||||
}
|
}
|
||||||
hosts, err := opts.GetAllRouteableHTTPHosts()
|
hosts, mcpHosts, err := opts.GetAllRouteableHTTPHosts()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, cmp.Diff(mcpHosts, map[string]bool{"from4.example.com:443": true, "from4.example.com": true}))
|
||||||
|
|
||||||
assert.Equal(t, []string{
|
assert.Equal(t, []string{
|
||||||
"authenticate.example.com",
|
"authenticate.example.com",
|
||||||
|
@ -916,6 +920,8 @@ func TestOptions_GetAllRouteableHTTPHosts(t *testing.T) {
|
||||||
"from2.example.com:443",
|
"from2.example.com:443",
|
||||||
"from3.example.com",
|
"from3.example.com",
|
||||||
"from3.example.com:443",
|
"from3.example.com:443",
|
||||||
|
"from4.example.com",
|
||||||
|
"from4.example.com:443",
|
||||||
}, hosts)
|
}, hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/handlers"
|
"github.com/pomerium/pomerium/internal/handlers"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
|
"github.com/pomerium/pomerium/internal/mcp"
|
||||||
"github.com/pomerium/pomerium/internal/middleware"
|
"github.com/pomerium/pomerium/internal/middleware"
|
||||||
"github.com/pomerium/pomerium/internal/telemetry"
|
"github.com/pomerium/pomerium/internal/telemetry"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
|
@ -79,5 +80,10 @@ func (srv *Server) mountCommonEndpoints(root *mux.Router, cfg *config.Config) er
|
||||||
root.Handle("/.well-known/pomerium/", traceHandler(handlers.WellKnownPomerium(authenticateURL)))
|
root.Handle("/.well-known/pomerium/", traceHandler(handlers.WellKnownPomerium(authenticateURL)))
|
||||||
root.Path("/.well-known/pomerium/jwks.json").Methods(http.MethodGet).Handler(traceHandler(handlers.JWKSHandler(signingKey)))
|
root.Path("/.well-known/pomerium/jwks.json").Methods(http.MethodGet).Handler(traceHandler(handlers.JWKSHandler(signingKey)))
|
||||||
root.Path(urlutil.HPKEPublicKeyPath).Methods(http.MethodGet).Handler(traceHandler(hpke_handlers.HPKEPublicKeyHandler(hpkePublicKey)))
|
root.Path(urlutil.HPKEPublicKeyPath).Methods(http.MethodGet).Handler(traceHandler(hpke_handlers.HPKEPublicKeyHandler(hpkePublicKey)))
|
||||||
|
|
||||||
|
root.Path("/.well-known/oauth-authorization-server").
|
||||||
|
Methods(http.MethodGet, http.MethodOptions).
|
||||||
|
Handler(mcp.AuthorizationServerMetadataHandler(mcp.DefaultPrefix))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
11
internal/mcp/handler.go
Normal file
11
internal/mcp/handler.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultPrefix = "/.pomerium/mcp"
|
||||||
|
|
||||||
|
authorizationEndpoint = "/authorize"
|
||||||
|
oauthCallbackEndpoint = "/oauth/callback"
|
||||||
|
registerEndpoint = "/register"
|
||||||
|
revocationEndpoint = "/revoke"
|
||||||
|
tokenEndpoint = "/token"
|
||||||
|
)
|
129
internal/mcp/handler_metadata.go
Normal file
129
internal/mcp/handler_metadata.go
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/rs/cors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthorizationServerMetadata represents the OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
||||||
|
type AuthorizationServerMetadata struct {
|
||||||
|
// Issuer is REQUIRED. The authorization server's issuer identifier, a URL using the "https" scheme with no query or fragment.
|
||||||
|
Issuer string `json:"issuer"`
|
||||||
|
|
||||||
|
// AuthorizationEndpoint is the URL of the authorization server's authorization endpoint. REQUIRED unless no grant types use the authorization endpoint.
|
||||||
|
AuthorizationEndpoint string `json:"authorization_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// TokenEndpoint is the URL of the authorization server's token endpoint. REQUIRED unless only the implicit grant type is supported.
|
||||||
|
TokenEndpoint string `json:"token_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// JwksURI is OPTIONAL. URL of the authorization server's JWK Set document.
|
||||||
|
JwksURI string `json:"jwks_uri,omitempty"`
|
||||||
|
|
||||||
|
// RegistrationEndpoint is OPTIONAL. URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint.
|
||||||
|
RegistrationEndpoint string `json:"registration_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// ScopesSupported is RECOMMENDED. JSON array of supported OAuth 2.0 "scope" values.
|
||||||
|
ScopesSupported []string `json:"scopes_supported,omitempty"`
|
||||||
|
|
||||||
|
// ResponseTypesSupported is REQUIRED. JSON array of supported OAuth 2.0 "response_type" values.
|
||||||
|
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||||
|
|
||||||
|
// ResponseModesSupported is OPTIONAL. JSON array of supported OAuth 2.0 "response_mode" values. Default: ["query", "fragment"].
|
||||||
|
ResponseModesSupported []string `json:"response_modes_supported,omitempty"`
|
||||||
|
|
||||||
|
// GrantTypesSupported is OPTIONAL. JSON array of supported OAuth 2.0 grant type values. Default: ["authorization_code", "implicit"].
|
||||||
|
GrantTypesSupported []string `json:"grant_types_supported,omitempty"`
|
||||||
|
|
||||||
|
// TokenEndpointAuthMethodsSupported is OPTIONAL. JSON array of client authentication methods supported by the token endpoint. Default: "client_secret_basic".
|
||||||
|
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
|
||||||
|
// TokenEndpointAuthSigningAlgValuesSupported is OPTIONAL. JSON array of JWS signing algorithms supported by the token endpoint for JWT client authentication.
|
||||||
|
TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
|
||||||
|
// ServiceDocumentation is OPTIONAL. URL of a page with human-readable information for developers.
|
||||||
|
ServiceDocumentation string `json:"service_documentation,omitempty"`
|
||||||
|
|
||||||
|
// UILocalesSupported is OPTIONAL. JSON array of supported languages and scripts for the UI, as BCP 47 language tags.
|
||||||
|
UILocalesSupported []string `json:"ui_locales_supported,omitempty"`
|
||||||
|
|
||||||
|
// OpPolicyURI is OPTIONAL. URL for the authorization server's policy on client data usage.
|
||||||
|
OpPolicyURI string `json:"op_policy_uri,omitempty"`
|
||||||
|
|
||||||
|
// OpTosURI is OPTIONAL. URL for the authorization server's terms of service.
|
||||||
|
OpTosURI string `json:"op_tos_uri,omitempty"`
|
||||||
|
|
||||||
|
// RevocationEndpoint is OPTIONAL. URL of the authorization server's OAuth 2.0 revocation endpoint.
|
||||||
|
RevocationEndpoint string `json:"revocation_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// RevocationEndpointAuthMethodsSupported is OPTIONAL. JSON array of client authentication methods supported by the revocation endpoint. Default: "client_secret_basic".
|
||||||
|
RevocationEndpointAuthMethodsSupported []string `json:"revocation_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
|
||||||
|
// RevocationEndpointAuthSigningAlgValuesSupported is OPTIONAL. JSON array of JWS signing algorithms supported by the revocation endpoint for JWT client authentication.
|
||||||
|
RevocationEndpointAuthSigningAlgValuesSupported []string `json:"revocation_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
|
||||||
|
// IntrospectionEndpoint is OPTIONAL. URL of the authorization server's OAuth 2.0 introspection endpoint.
|
||||||
|
IntrospectionEndpoint string `json:"introspection_endpoint,omitempty"`
|
||||||
|
|
||||||
|
// IntrospectionEndpointAuthMethodsSupported is OPTIONAL. JSON array of client authentication methods supported by the introspection endpoint.
|
||||||
|
IntrospectionEndpointAuthMethodsSupported []string `json:"introspection_endpoint_auth_methods_supported,omitempty"`
|
||||||
|
|
||||||
|
// IntrospectionEndpointAuthSigningAlgValuesSupported is OPTIONAL. JSON array of JWS signing algorithms supported by the introspection endpoint for JWT client authentication.
|
||||||
|
IntrospectionEndpointAuthSigningAlgValuesSupported []string `json:"introspection_endpoint_auth_signing_alg_values_supported,omitempty"`
|
||||||
|
|
||||||
|
// CodeChallengeMethodsSupported is OPTIONAL. JSON array of PKCE code challenge methods supported by this authorization server.
|
||||||
|
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AuthorizationServerMetadataHandler(prefix string) http.HandlerFunc {
|
||||||
|
c := cors.New(cors.Options{
|
||||||
|
AllowedMethods: []string{http.MethodGet, http.MethodOptions},
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedHeaders: []string{"mcp-protocol-version"},
|
||||||
|
})
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.Use(c.Handler)
|
||||||
|
r.Methods(http.MethodGet).HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
meta := getAuthorizationServerMetadata(r.Host, prefix)
|
||||||
|
_ = json.NewEncoder(w).Encode(meta)
|
||||||
|
})
|
||||||
|
r.Methods(http.MethodOptions).HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
})
|
||||||
|
return http.HandlerFunc(r.ServeHTTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationServerMetadata(host, prefix string) AuthorizationServerMetadata {
|
||||||
|
baseURL := url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
P := func(path string) string {
|
||||||
|
u := baseURL
|
||||||
|
u.Path = path
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthorizationServerMetadata{
|
||||||
|
Issuer: P("/"),
|
||||||
|
ServiceDocumentation: "https://pomerium.com/docs",
|
||||||
|
AuthorizationEndpoint: P(path.Join(prefix, authorizationEndpoint)),
|
||||||
|
ResponseTypesSupported: []string{"code"},
|
||||||
|
CodeChallengeMethodsSupported: []string{"S256"},
|
||||||
|
TokenEndpoint: P(path.Join(prefix, tokenEndpoint)),
|
||||||
|
TokenEndpointAuthMethodsSupported: []string{"none"},
|
||||||
|
GrantTypesSupported: []string{"authorization_code", "refresh_token"},
|
||||||
|
RevocationEndpoint: P(path.Join(prefix, revocationEndpoint)),
|
||||||
|
RevocationEndpointAuthMethodsSupported: []string{"client_secret_post"},
|
||||||
|
RegistrationEndpoint: P(path.Join(prefix, registerEndpoint)),
|
||||||
|
ScopesSupported: []string{"openid", "offline"},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue