mcp: send request body to authorize (#5660)

## Summary

In order to inspect the MCP requests and use the request payload in the
authorization decisions,
configure `ext_authz` to send the request payload as well. 

the body then would be available for inspection as it would contain the
json-rpc message like
```json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"_meta":{"progressToken":1},"name":"list_tables","arguments":{}}}
```

## Related issues

Ref:
https://linear.app/pomerium/issue/ENG-2393/mcp-authorize-each-incoming-request-to-an-mcp-route

## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [ ] reference any related issues
- [ ] updated unit tests
- [ ] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [ ] ready for review
This commit is contained in:
Denis Mishin 2025-06-20 11:45:00 -07:00 committed by GitHub
parent b0c2e2dede
commit 55dd6ba7d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 1 deletions

View file

@ -21,6 +21,22 @@ func PerFilterConfigExtAuthzContextExtensions(authzContextExtensions map[string]
})
}
// PerFilterConfigExtAuthzContextExtensionsWithBody returns a per-filter config for ext authz that
// sets context extensions and includes the request body.
func PerFilterConfigExtAuthzContextExtensionsWithBody(mcpRequestBodyMaxBytes uint32, authzContextExtensions map[string]string) *anypb.Any {
return marshalAny(&envoy_extensions_filters_http_ext_authz_v3.ExtAuthzPerRoute{
Override: &envoy_extensions_filters_http_ext_authz_v3.ExtAuthzPerRoute_CheckSettings{
CheckSettings: &envoy_extensions_filters_http_ext_authz_v3.CheckSettings{
ContextExtensions: authzContextExtensions,
WithRequestBody: &envoy_extensions_filters_http_ext_authz_v3.BufferSettings{
MaxRequestBytes: mcpRequestBodyMaxBytes,
AllowPartialMessage: true,
},
},
},
})
}
// PerFilterConfigExtAuthzDisabled returns a per-filter config for ext authz that disables ext-authz.
func PerFilterConfigExtAuthzDisabled() *anypb.Any {
return marshalAny(&envoy_extensions_filters_http_ext_authz_v3.ExtAuthzPerRoute{

View file

@ -325,8 +325,13 @@ func (b *Builder) buildRouteForPolicyAndMatch(
PerFilterConfigExtAuthzName: PerFilterConfigExtAuthzDisabled(),
}
} else {
extAuthzOpts := MakeExtAuthzContextExtensions(false, routeID, routeChecksum)
extAuthzCfg := PerFilterConfigExtAuthzContextExtensions(extAuthzOpts)
if policy.IsMCPServer() {
extAuthzCfg = PerFilterConfigExtAuthzContextExtensionsWithBody(policy.MCP.GetMaxRequestBytes(), extAuthzOpts)
}
route.TypedPerFilterConfig = map[string]*anypb.Any{
PerFilterConfigExtAuthzName: PerFilterConfigExtAuthzContextExtensions(MakeExtAuthzContextExtensions(false, routeID, routeChecksum)),
PerFilterConfigExtAuthzName: extAuthzCfg,
}
luaMetadata["remove_pomerium_cookie"] = &structpb.Value{
Kind: &structpb.Value_StringValue{

View file

@ -216,6 +216,15 @@ type MCP struct {
UpstreamOAuth2 *UpstreamOAuth2 `mapstructure:"upstream_oauth2" yaml:"upstream_oauth2,omitempty" json:"upstream_oauth2,omitempty"`
// PassUpstreamAccessToken indicates whether to pass the upstream access token in the `Authorization: Bearer` header that is suitable for calling the MCP routes
PassUpstreamAccessToken bool `mapstructure:"pass_upstream_access_token" yaml:"pass_upstream_access_token,omitempty" json:"pass_upstream_access_token,omitempty"`
// MaxRequestBytes is the maximum request body size in bytes that can be sent to the MCP server
MaxRequestBytes *uint32 `mapstructure:"max_request_bytes" yaml:"max_request_bytes,omitempty" json:"max_request_bytes,omitempty"`
}
func (p *MCP) GetMaxRequestBytes() uint32 {
if p == nil || p.MaxRequestBytes == nil {
return 4 * 1024
}
return *p.MaxRequestBytes
}
// HasUpstreamOAuth2 checks if the route is for the MCP Server and if it has an upstream OAuth2 configuration