Skip to content

mcp: JWT scope based authorization#1482

Merged
nacx merged 30 commits intoenvoyproxy:mainfrom
zhaohuabing:mcp-authorization
Dec 17, 2025
Merged

mcp: JWT scope based authorization#1482
nacx merged 30 commits intoenvoyproxy:mainfrom
zhaohuabing:mcp-authorization

Conversation

@zhaohuabing
Copy link
Copy Markdown
Member

@zhaohuabing zhaohuabing commented Nov 4, 2025

Description

This PR introduces MCP spec compatible scope based authorization for MCPRoutes.

According to the 2025-11-25 version of the MCP spec, the MCP Gateway should enforce scope-based authorization on behalf of the backend MCP server, and include the required scopes in the WWW-Authenticate header of the 403 response if authoriation fails.

https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors

When a client makes a request with an access token with insufficient scope during runtime operations, the server SHOULD respond with:
HTTP 403 Forbidden status code
WWW-Authenticate header with the Bearer scheme and additional parameters:
error="insufficient_scope" - indicating the specific type of authorization failure
scope="required_scope1 required_scope2" - specifying the minimum scopes needed for the operation
resource_metadata - the URI of the Protected Resource Metadata document (for consistency with 401 responses)
error_description (optional) - human-readable description of the error

Example:

spec:
  ...
  securityPolicy:
    oauth:
      ...
    authorization:
      rules:
        - source:
            jwt:
              scopes:
                - echo
          target:
            tools:
              - backend: mcp-backend-authorization
                toolName: echo
                when: args.text.matches("^Hello, .*!$")
        - source:
            jwt:
              scopes:
                - sum
          target:
            tools:
              - backend: mcp-backend-authorization
                toolName: sum

Reference: https://modelcontextprotocol.io/specification/draft/basic/authorization#scope-challenge-handling

Implements #1459

@zhaohuabing zhaohuabing requested a review from a team as a code owner November 4, 2025 07:14
@zhaohuabing zhaohuabing marked this pull request as draft November 4, 2025 07:14
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Nov 4, 2025
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Nov 4, 2025

Codecov Report

❌ Patch coverage is 63.42857% with 64 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.17%. Comparing base (2c3b46f) to head (4b8bc39).
⚠️ Report is 129 commits behind head on main.

Files with missing lines Patch % Lines
internal/controller/gateway.go 0.00% 30 Missing and 1 partial ⚠️
internal/mcpproxy/authorization.go 81.74% 16 Missing and 7 partials ⚠️
internal/mcpproxy/handlers.go 20.00% 7 Missing and 1 partial ⚠️
internal/mcpproxy/config.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1482      +/-   ##
==========================================
- Coverage   83.46%   83.17%   -0.29%     
==========================================
  Files         138      139       +1     
  Lines       12540    12711     +171     
==========================================
+ Hits        10466    10573     +107     
- Misses       1440     1494      +54     
- Partials      634      644      +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zhaohuabing zhaohuabing force-pushed the mcp-authorization branch 2 times, most recently from d1ea251 to 877d598 Compare November 4, 2025 07:45
@zhaohuabing zhaohuabing force-pushed the mcp-authorization branch 2 times, most recently from 4d2e8d5 to 0ccf737 Compare November 4, 2025 08:05
@nacx nacx self-assigned this Nov 4, 2025
@zhaohuabing zhaohuabing force-pushed the mcp-authorization branch 4 times, most recently from 29db138 to 2dd862c Compare November 28, 2025 09:32
@zhaohuabing zhaohuabing force-pushed the mcp-authorization branch 3 times, most recently from 2167726 to a95087c Compare December 2, 2025 02:14
@zhaohuabing zhaohuabing marked this pull request as ready for review December 2, 2025 03:21
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Dec 2, 2025
@zhaohuabing zhaohuabing force-pushed the mcp-authorization branch 5 times, most recently from b77c937 to f8a07cc Compare December 3, 2025 03:19
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxLength=4096
// +optional
Arguments *string `json:"arguments,omitempty"`
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing this with @nacx offline, we chose CEL for argument evaluation.

CEL is easier to write and understand than regex evaluation when the arguments are complex object types.

For example:

To check against this argument:

{
  "key-level1": {
    "key-level2": {
      "key-level3": "value"
    }
  }
}

The regex match:

/"key-level1"\s*:\s*\{\s*"key-level2"\s*:\s*\{\s*"key-level3"\s*:\s*"value"\s*\}\s*\}/gm

The CEL match:

args["key-level1"]["key-level2"]["key-level3"] == "value"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about renaming this to condition? If we do this, we could also easily support meta in addition to args to have policies based on the contents of the metadata.

Copy link
Copy Markdown
Member Author

@zhaohuabing zhaohuabing Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should meta be inside the ToolCall, or MCPAuthorizationTarget?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh meta is part of the CallToolParams - then condition is a good.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it again, condition feels a bit vague and also used in the status field, some alternatives:

  • When
  • Predicate
  • Matcher

When reads naturally in the context of authorization rules:

tools:
  - backend: backend1
    tool: listFiles
    when: args.folder == "restricted"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the when better than the other two options. It is also used for additional conditions in Istio AuthorizationPolicies, so it's not a completely foreign name.

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
} else {
claims := jwt.MapClaims{}
// JWT verification is performed by Envoy before reaching here. So we only need to parse the token without verification.
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err != nil {

Check failure

Code scanning / CodeQL

Missing JWT signature check High

This JWT is parsed without verification and received from
this user-controlled source
.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explained in the comment why verification is not needed here

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
@zhaohuabing
Copy link
Copy Markdown
Member Author

zhaohuabing commented Dec 4, 2025

WDYT about renaming this to condition? If we do this, we could also easily support meta in addition to args to have policies based on the contents of the metadata.

https://github.com/envoyproxy/ai-gateway/pull/1482/files#r2587966887

Thinking about it again, condition feels a bit vague and also used in the status field, some alternatives:

  • When
  • Predicate
  • Matcher

When reads naturally in the context of authorization rules:

tools:
  - backend: backend1
    tool: listFiles
    when: args.folder == "restricted"

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
Copy link
Copy Markdown
Member

@nacx nacx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just one final comment about validations and API semantics, but this looks good!

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
@zhaohuabing zhaohuabing marked this pull request as draft December 16, 2025 01:56
Signed-off-by: Huabing(Robin) Zhao <zhaohuabing@gmail.com>
@zhaohuabing zhaohuabing marked this pull request as ready for review December 16, 2025 13:37
Signed-off-by: Huabing(Robin) Zhao <zhaohuabing@gmail.com>
@zhaohuabing zhaohuabing requested a review from nacx December 16, 2025 14:04
@zhaohuabing
Copy link
Copy Markdown
Member Author

The CI failure is not related to this PR:

We'll need to create a PAT to access the Github MCP server.

time=2025-12-16T23:48:41.239Z level=ERROR msg="failed to create MCP session" component=mcp-proxy backend=github error="MCP initialize request failed with status code 400 and body=bad request: GitHub App Server-To-Server Tokens are not supported for this endpoint\n"

@nacx
Copy link
Copy Markdown
Member

nacx commented Dec 17, 2025

Yeah, looks like there are changes on the GH MCP: github/github-mcp-server#1610

@nacx
Copy link
Copy Markdown
Member

nacx commented Dec 17, 2025

/retest

@nacx nacx merged commit eb7f7b7 into envoyproxy:main Dec 17, 2025
47 of 49 checks passed
@zhaohuabing zhaohuabing deleted the mcp-authorization branch December 17, 2025 10:19
missBerg pushed a commit to missBerg/ai-gateway that referenced this pull request Dec 20, 2025
…oxy#1608)

**Description**

Add the configured scopes to the `WWW-Authenticate` headers. At
initialization time, which is when the first authentication will occur,
we don't have enough information to provide a fine-grained list of
scopes, so the best we can do is to default to the ones defined in the
protected resource metadata.

**Related Issues/PRs (if applicable)**

Fixes envoyproxy#1578

The addition of the header on 403 requests is implemented in
envoyproxy#1482, but this issue can
be closed as soon as this PR is merged, because we'll be compatible with
the latest spec.

**Special notes for reviewers (if applicable)**

cc @zhaohuabing can you take a look?

Signed-off-by: Ignasi Barrera <nacx@apache.org>
Signed-off-by: Erica Hughberg <erica.sundberg.90@gmail.com>
missBerg pushed a commit to missBerg/ai-gateway that referenced this pull request Dec 20, 2025
**Description**

This PR introduces MCP spec compatible scope based authorization for
MCPRoutes.

According to the 2025-11-25 version of the MCP spec, the MCP Gateway
should enforce scope-based authorization on behalf of the backend MCP
server, and include the required scopes in the `WWW-Authenticate` header
of the 403 response if authoriation fails.

https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors

> When a client makes a request with an access token with insufficient
scope during runtime operations, the server SHOULD respond with:
HTTP 403 Forbidden status code
WWW-Authenticate header with the Bearer scheme and additional
parameters:
error="insufficient_scope" - indicating the specific type of
authorization failure
scope="required_scope1 required_scope2" - specifying the minimum scopes
needed for the operation
resource_metadata - the URI of the Protected Resource Metadata document
(for consistency with 401 responses)
error_description (optional) - human-readable description of the error

Example:

```yaml
spec:
  ...
  securityPolicy:
    oauth:
      ...
    authorization:
      rules:
        - source:
            jwt:
              scopes:
                - echo
          target:
            tools:
              - backend: mcp-backend-authorization
                toolName: echo
                when: args.text.matches("^Hello, .*!$")
        - source:
            jwt:
              scopes:
                - sum
          target:
            tools:
              - backend: mcp-backend-authorization
                toolName: sum
```

Reference:
https://modelcontextprotocol.io/specification/draft/basic/authorization#scope-challenge-handling

Implements envoyproxy#1459

---------

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
Signed-off-by: Huabing(Robin) Zhao <zhaohuabing@gmail.com>
Co-authored-by: Ignasi Barrera <ignasi@tetrate.io>
Signed-off-by: Erica Hughberg <erica.sundberg.90@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants