Skip to content

Conversation

@npolshakova
Copy link
Contributor

Description

Motivation:
Allows users to configure MCP authentication for MCP backends.

What changed:
Requires these PRs to go in first :)

Fixes #12469

Change Type

/kind feature

Changelog

Added support for MCP authentication for agentgateway.

Additional Notes

For testing MCP Authentication with keycloak, apply the remote-authn-keycloak.yaml, common.yaml and keycloak.yaml manifests along with the curl_pod.yaml from the e2e test setup.

  1. Send a request without authorization header:
curl -v -X POST http://gw.default.svc.cluster.local:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json,text/event-stream" \
  -d '{
    "method": "tools/call",
    "params": {
      "name": "fetch",
      "arguments": { "url": "http://google.com" }
    }
  }'
  1. You should get a 401 Unauthorized response:
< HTTP/1.1 401 Unauthorized
< www-authenticate: Bearer resource_metadata="http://mcp-website-fetcher.default.svc.cluster.local/.well-known/oauth-protected-resource/mcp"
< content-type: application/json
< content-length: 65
< date: Thu, 20 Nov 2025 14:32:26 GMT
< 
* Connection #0 to host localhost left intact
{"error":"unauthorized","error_description":"JWT token required"}%
  1. From the curl pod you should be able to get a token from keycloak under access_token and save it as TOKEN:
curl -s -X POST "http://keycloak.default:7080/realms/mcp/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=mcp_proxy" \
  -d "client_secret=supersecret"
  1. Then use the token to make the request:
curl -v -X POST http://gw.default.svc.cluster.local:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json,text/event-stream" \
  -H "Authorization: Bearer <TOKEN>" \
  -d '{
    "method": "tools/call",
    "params": {
      "name": "fetch",
      "arguments": { "url": "http://google.com" }
    }
  }'

You should no longer see the 401 error!

You can also test this with the MCP inspector:

  1. Run npx modelcontextprotocol/inspector#0.16.2
  2. Open the inspector UI
  3. Attempt to connect to the port-forwarded gateway (http://localhost:8080/) without the token using Streamable HTTP
  4. Set the TOKEN under the API Token Authentication field, then click Connect
  5. Go to the tools tab and test the fetch tool with a URL of your choice

Example without the token:
image

Example with token:
image

Follow ups:

  • Add mode configuration to support non-optional jwt modes

Copilot AI review requested due to automatic review settings November 24, 2025 18:49
@npolshakova npolshakova mentioned this pull request Nov 24, 2025
@gateway-bot gateway-bot added kind/feature Categorizes issue or PR as related to a new feature. release-note labels Nov 24, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds MCP (Model Context Protocol) authentication support to the agentgateway, enabling JWT-based authentication for MCP backends with support for Auth0 and Keycloak identity providers.

Key changes:

  • Added MCP authentication policy configuration with JWKS validation
  • Implemented Auth0 mock server for e2e testing
  • Extended JWKS store controller to handle MCP authentication JWKS sources

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
api/v1alpha1/agentgateway_policy_types.go Added MCPAuthentication type with issuer, audiences, JWKS, and IDP configuration
api/v1alpha1/zz_generated.deepcopy.go Generated deep copy methods for new authentication types
pkg/agentgateway/plugins/backend_policies.go Implemented translateBackendMCPAuthentication to convert policy to agentgateway format
internal/kgateway/agentjwksstore/jwks_store_controller.go Extended to fetch JWKS from both AgentgatewayPolicy and AgentgatewayBackend MCP auth configs
install/helm/kgateway-crds/templates/*.yaml Updated CRDs with MCP authentication schema
test/e2e/features/agentgateway/mcp/* Added e2e tests for MCP authentication with Auth0 mock
hack/dummy-auth0/* New Python-based Auth0 mock server for testing OAuth flows
go.mod Uses local replace directive for agentgateway dependency
Makefile Added build targets for dummy-auth0 Docker image

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@npolshakova npolshakova force-pushed the mcp-authn-rebase branch 3 times, most recently from 5098478 to 4eedaac Compare November 25, 2025 20:54
// McpIDP specifies the identity provider to use for authentication
// +kubebuilder:validation:Enum=Auth0;Keycloak
// +optional
McpIDP *McpIDP `json:"idp,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need to know the IDP type? i.e. why isn't the issuer enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

not related to the PR, is the above a constraint in our implementation, or is it coming from a spec? The spec i was able to find (https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#third-party-authorization-flow) is pretty minimal when it comes to third-party auth providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Our implementation- the idp and issuers are both determining the path in the line I sent earlier.

}

// enqueue Backend MCP authentication JWKS (if present)
if p.Spec.Backend != nil && p.Spec.Backend.MCP != nil && p.Spec.Backend.MCP.Authentication != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should look more closely at intoducing a jwks backend for AgentgatewayPolicy (see notes in #13014 for examples), as it would unify tls/traffic config UX and handling for fetching of remote jwks.

// TODO: implement
// ResourceMetadata defines the metadata to use for MCP resources.
// +optional
ResourceMetadata map[string]string `json:"resourceMetadata"`
Copy link
Contributor

Choose a reason for hiding this comment

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

should not be map[string]string but rather map[string]JsonValue. I think the appropriate way to represent this is map[string]apiextensionsv1.JSON in kubebuilder but not 100% sure it works inside a map. You can see similar in FieldDefault

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks like ResourceMetadata map[string]apiextensionsv1.JSON worked with kubebuilder 🎉

// McpIDP specifies the identity provider to use for authentication
// +kubebuilder:validation:Enum=Auth0;Keycloak
// +optional
McpIDP *McpIDP `json:"idp,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: provider or identityProvider I think may be more fitting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll use provider since it's already under the mcp.authentication fields. I think identityProvider is a little too long

@@ -0,0 +1,18 @@
FROM python:3.11-slim
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: would be great to not have so many different mocks. can we just merge this in with the Golang mock IDP?

Have a separate image, especially a python one, is going to bloat our CI times

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ideally, yes- I think @shashankram is also looking at adding a provider for e2e tests on the envoy side. The current golang mock idp we're using for the jwt tests is pretty jwt specific (it just provides the different jwks to fetch, doesn't have the paths we need to test the mcp auth flow integration). I think we should discuss offline and consolitdate into having one e2e idp provider that ideally works for all these scenarios.

Copy link
Contributor

Choose a reason for hiding this comment

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

I did a minimal thing just to support remote jwks e2e tests; we could extend the fake idp, or even have a real idp with fake data (like using zitadel + preset accounts and jwks; not sure re: jwts, those may need to be generated).


idp := api.BackendPolicySpec_McpAuthentication_AUTH0
if authnPolicy.McpIDP != nil && *authnPolicy.McpIDP == v1alpha1.Keycloak {
idp = api.BackendPolicySpec_McpAuthentication_KEYCLOAK
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs a proto change, but we need a way to represent "no provider".

Provider means "this provider is no implementing the proper RFCs and needs workarounds" so ideally a user doesn't need one at all. And we should not default to keycloak

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It currently defaults to auth0, but agree- it would be ideal to not require a provider. Will create a follow up for this to make the dataplane change along with the mode configuration to support non-optional jwt modes

extraResourceMetadata = make(map[string]*structpb.Value)
}
var parsed any
if err := json.Unmarshal([]byte(v), &parsed); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

if we do my commet on the API we don't need this or the json-in-string from users

Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: npolshakova <[email protected]>
@howardjohn howardjohn added this pull request to the merge queue Dec 2, 2025
Merged via the queue into kgateway-dev:main with commit a32a0f7 Dec 2, 2025
30 checks passed
sheidkamp pushed a commit to sheidkamp/kgateway that referenced this pull request Dec 3, 2025
howardjohn pushed a commit to howardjohn/kgateway that referenced this pull request Dec 5, 2025
Signed-off-by: npolshakova <[email protected]>
Signed-off-by: John Howard <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature. release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agentgateway: Support MCP Authorization in kgateway

5 participants