This feature is currently in Tech Preview.
AI Gateway Enterprise: This plugin is only available as part of our AI Gateway Enterprise offering.
The AI MCP OAuth2 plugin secures Model Context Protocol (MCP) traffic on AI Gateway using OAuth 2.0 specification for MCP servers. It ensures only authorized MCP clients can access protected MCP servers, and acts as a crucial security layer for MCP servers.
Breaking change
v3.13+The MCP OAuth2 plugin now treats all incoming traffic as MCP requests to address a potential authentication bypass vulnerability.
Do not use this plugin with the AI MCP Proxy plugin in
conversion-listenermode on the same route. Non-MCP requests will fail.Use MCP OAuth2 with MCP Proxy in
listenerorpassthrough-listenermodes. For REST API exposure, configure MCP Proxy inconversion-onlymode on a separate route.
Purpose and core functionality
The plugin provides OAuth 2.0 authentication for MCP traffic, allowing MCP clients to safely request access. It validates that access tokens are issued specifically for the target MCP server, ensuring only authorized requests are accepted. To reduce the risk of token theft or confused deputy attacks, the plugin does not pass access tokens to upstream services.
The plugin performs three core functions:
- Validates incoming MCP requests by verifying access tokens from an external Authorization Server.
- Extracts claims from validated tokens and forwards them to upstream MCP services via headers.
- Ensures compliance with MCP authorization requirements based on OAuth 2.1.
Authorization flow
The plugin follows the following authorization flow:
- AI Gateway acts as the Resource Server, enforcing access control.
- The MCP clients send requests with a valid
Authorization: Bearer <access-token>header. - The plugin validates tokens, checks the intended audience, and blocks invalid or expired tokens with a
401 Unauthorized. - Access tokens are not forwarded to upstream services by default, protecting against token theft or confused deputy attacks.
sequenceDiagram participant C as MCP client participant K as AI MCP OAuth2 plugin participant AS as Authorization server participant U as Upstream MCP server C->>K: Discover protected resource metadata activate K K-->>C: Protected resource metadata (includes auth server address) deactivate K C->>AS: Request access token activate AS AS-->>C: Access token deactivate AS C->>K: MCP auth request activate K K->>AS: Introspect token activate AS AS-->>K: Valid / invalid deactivate AS alt If token valid K->>U: Forward request with claims as headers activate U U-->>K: MCP server response deactivate U K-->>C: MCP response else If token invalid K-->>C: 401 Unauthorized end deactivate K
Plugin execution
The AI MCP OAuth2 plugin is designed to secure MCP traffic as early as possible in the request lifecycle to prevent unauthorized access before any AI-specific processing occurs.
Note: Like, the AI MCP Proxy plugin, the AI MCP OAuth2 plugin is not invoked as part of an LLM request flow.
Instead, it is registered and executed as a regular plugin, allowing it to capture MCP traffic independently of LLM request flow. The AI MCP OAuth2 plugin can be used on its own for upstream MCP proxying or in combination with the AI MCP Proxy plugin when request/response conversion is needed.
Token validation methods v3.14+
The plugin supports two token validation methods. When introspection is configured, it is always used. JWKS is only used when no introspection endpoint is configured.
-
Introspection: Set
config.introspection_endpointto have the plugin call the authorization server to validate opaque tokens. Requiresconfig.client_idwhenconfig.client_authisclient_secret_basicorclient_secret_post. -
JWKS: Set
config.jwks_endpointto validate signed JWTs locally using the authorization server’s public keys. If not set, the plugin attempts to discover the JWKS URI from the authorization server metadata.
Claim forwarding
The plugin can extract claims from a validated token and forward them to the upstream MCP server as HTTP headers. Two approaches are available, and they are mutually exclusive.
Top-level claims
Use config.claim_to_header to map top-level token claims to upstream headers. Each entry requires a claim name and a header name:
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
claim_to_header:
- claim: sub
header: X-User-Id
- claim: email
header: X-User-Emailcurl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"claim_to_header": [
{
"claim": "sub",
"header": "X-User-Id"
},
{
"claim": "email",
"header": "X-User-Email"
}
]
}
}
'Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"claim_to_header": [
{
"claim": "sub",
"header": "X-User-Id"
},
{
"claim": "email",
"header": "X-User-Email"
}
]
}
}
'echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name:
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
labels:
global: 'true'
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
claim_to_header:
- claim: sub
header: X-User-Id
- claim: email
header: X-User-Email
plugin: ai-mcp-oauth2
" | kubectl apply -f -resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = "https://api.example.com/mcp"
authorization_servers = ["https://auth.example.com"]
claim_to_header = [
{
claim = "sub"
header = "X-User-Id"
},
{
claim = "email"
header = "X-User-Email"
} ]
}
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
}Nested claims v3.14+
Use config.upstream_headers to map claims at any depth in the token payload using a path array. This field is mutually exclusive with claim_to_header:
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
upstream_headers:
- header: X-Org-Id
path:
- org
- id
- header: X-User-Role
path:
- realm_access
- rolescurl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"upstream_headers": [
{
"header": "X-Org-Id",
"path": [
"org",
"id"
]
},
{
"header": "X-User-Role",
"path": [
"realm_access",
"roles"
]
}
]
}
}
'Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"upstream_headers": [
{
"header": "X-Org-Id",
"path": [
"org",
"id"
]
},
{
"header": "X-User-Role",
"path": [
"realm_access",
"roles"
]
}
]
}
}
'echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name:
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
labels:
global: 'true'
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
upstream_headers:
- header: X-Org-Id
path:
- org
- id
- header: X-User-Role
path:
- realm_access
- roles
plugin: ai-mcp-oauth2
" | kubectl apply -f -resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = "https://api.example.com/mcp"
authorization_servers = ["https://auth.example.com"]
upstream_headers = [
{
header = "X-Org-Id"
path = ["org", "id"]
},
{
header = "X-User-Role"
path = ["realm_access", "roles"]
} ]
}
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
}Consumer mapping v3.14+
The plugin can map token claims to Kong consumers and consumer groups, enabling consumer-based rate limiting, ACL, and other consumer-aware plugins to function with MCP traffic.
Consumer
Set config.consumer_claim to the path of the claim to use for consumer lookup. If multiple strings are provided, the plugin treats them as a nested path in the token payload. For example, ["sub"] maps the top-level sub claim, while ["realm_access", "user_id"] maps token.realm_access.user_id.
Use config.consumer_by to control which consumer fields are checked during lookup. Accepted values are id, username, and custom_id. Defaults to ["username", "custom_id"].
Set config.consumer_optional to true if you want the plugin to continue without failing when no matching consumer is found.
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
consumer_claim:
- sub
consumer_by:
- username
- custom_id
consumer_optional: falsecurl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"consumer_claim": [
"sub"
],
"consumer_by": [
"username",
"custom_id"
],
"consumer_optional": false
}
}
'Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"consumer_claim": [
"sub"
],
"consumer_by": [
"username",
"custom_id"
],
"consumer_optional": false
}
}
'echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name:
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
labels:
global: 'true'
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
consumer_claim:
- sub
consumer_by:
- username
- custom_id
consumer_optional: false
plugin: ai-mcp-oauth2
" | kubectl apply -f -resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = "https://api.example.com/mcp"
authorization_servers = ["https://auth.example.com"]
consumer_claim = ["sub"]
consumer_by = ["username", "custom_id"]
consumer_optional = false
}
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
}Consumer groups
Set config.consumer_groups_claim to the path of the claim containing the consumer group names. If multiple strings are provided, the plugin treats them as a nested path.
Set config.consumer_groups_optional to true to allow the request to proceed even if no matching consumer group is found.
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
consumer_groups_claim:
- groups
consumer_groups_optional: truecurl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"consumer_groups_claim": [
"groups"
],
"consumer_groups_optional": true
}
}
'Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "https://api.example.com/mcp",
"authorization_servers": [
"https://auth.example.com"
],
"consumer_groups_claim": [
"groups"
],
"consumer_groups_optional": true
}
}
'echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name:
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
labels:
global: 'true'
config:
resource: https://api.example.com/mcp
authorization_servers:
- https://auth.example.com
consumer_groups_claim:
- groups
consumer_groups_optional: true
plugin: ai-mcp-oauth2
" | kubectl apply -f -resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = "https://api.example.com/mcp"
authorization_servers = ["https://auth.example.com"]
consumer_groups_claim = ["groups"]
consumer_groups_optional = true
}
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
}Virtual credentials
When consumer mapping is not used, set config.credential_claim to derive a virtual credential from the token. This credential is used by plugins like rate-limiting to track usage. Defaults to ["sub"].
Token exchange v3.14+
Token exchange lets the plugin swap the client’s access token for a different token before forwarding the request to the upstream MCP server. This is useful when the upstream requires a token from a different authorization server or with different scopes.
Token exchange requires
config.passthrough_credentialsto be set totrue.
When config.token_exchange.enabled is true, the plugin performs the following after validating the incoming token:
sequenceDiagram participant C as MCP client participant K as AI MCP OAuth2 plugin participant AS as Authorization server participant TE as Token exchange endpoint participant U as Upstream MCP server C->>K: MCP request with Bearer token activate K K->>AS: Validate token (introspect / JWKS) activate AS AS-->>K: Token valid deactivate AS K->>TE: Token exchange request (subject_token = original token) activate TE TE-->>K: Exchanged access token deactivate TE K->>U: Forward request with exchanged token activate U U-->>K: MCP server response deactivate U K-->>C: MCP response deactivate K
The client_auth field controls how the plugin authenticates with the token exchange endpoint. Accepted values are client_secret_basic, client_secret_post, none, and inherit. When inherit is used, the plugin reuses the client_id and client_secret configured for the introspection endpoint.
When config.token_exchange.request.actor_token_source is set to header, provide the name of the header carrying the actor token in actor_token_header. When set to config, provide the static actor token value in actor_token.
Exchanged tokens are cached by default. Set config.token_exchange.cache.enabled to false to disable caching. The TTL defaults to 3600 seconds and is used when the token exchange endpoint does not return an expires_in value.
Token passthrough v3.14+
By default, the plugin strips the incoming access token before forwarding the request to the upstream MCP server, preventing token theft and confused deputy attacks. Set config.passthrough_credentials to true to keep the original token in the request.
Only enable token passthrough when the upstream MCP server explicitly requires the original access token, or when token exchange is configured.
