Token exchangev3.14+
Use token exchange to replace the incoming access token with a token issued by a different authorization server or with different scopes before the request reaches the upstream MCP server. This example also includes actor token configuration, which is required by identity providers like Okta that need the original token passed as the actor token during the exchange.
When token_exchange.enabled is true, the plugin:
- Validates the incoming Bearer token using introspection or JWKS.
- Calls the configured token exchange endpoint with the validated token as the subject token.
- Forwards the request to the upstream MCP server with the exchanged token.
Because the plugin is passing a credential to the upstream, passthrough_credentials
must be set to true when token exchange is enabled.
Exchanged tokens are cached by default. The TTL comes from the expires_in field in
the exchange response, or falls back to token_exchange.cache.ttl if the field is absent.
Environment variables
-
MCP_RESOURCE_URL: Resource identifier for the MCP server (for example,https://api.example.com/mcp). -
AUTHORIZATION_SERVER_URL: Authorization server URL used to validate the incoming token. -
INTROSPECTION_ENDPOINT_URL: Token introspection endpoint. Used by Kong Gateway to validate the incoming access token. -
CLIENT_ID: Client ID used by Kong Gateway when calling the introspection endpoint. -
CLIENT_SECRET: Client secret used by Kong Gateway when calling the introspection endpoint. -
TOKEN_EXCHANGE_ENDPOINT_URL: Token exchange endpoint URL (for example,https://exchange.example.com/oauth/token). -
EXCHANGE_CLIENT_ID: Client ID used by Kong Gateway when calling the token exchange endpoint. -
EXCHANGE_CLIENT_SECRET: Client secret used by Kong Gateway when calling the token exchange endpoint.
Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
config:
resource: ${{ env "DECK_MCP_RESOURCE_URL" }}
authorization_servers:
- ${{ env "DECK_AUTHORIZATION_SERVER_URL" }}
introspection_endpoint: ${{ env "DECK_INTROSPECTION_ENDPOINT_URL" }}
client_id: ${{ env "DECK_CLIENT_ID" }}
client_secret: ${{ env "DECK_CLIENT_SECRET" }}
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: ${{ env "DECK_TOKEN_EXCHANGE_ENDPOINT_URL" }}
client_auth: client_secret_basic
client_id: ${{ env "DECK_EXCHANGE_CLIENT_ID" }}
client_secret: ${{ env "DECK_EXCHANGE_CLIENT_SECRET" }}
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
Make the following request:
curl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
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": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
Make sure to replace the following placeholders with your own values:
-
region: Geographic region where your Kong Konnect is hosted and operates. -
KONNECT_TOKEN: Your Personal Access Token (PAT) associated with your Konnect account. -
controlPlaneId: Theidof the control plane.
See the Konnect API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name: ai-mcp-oauth2
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
konghq.com/tags: ''
labels:
global: 'true'
config:
resource: '$MCP_RESOURCE_URL'
authorization_servers:
- '$AUTHORIZATION_SERVER_URL'
introspection_endpoint: '$INTROSPECTION_ENDPOINT_URL'
client_id: '$CLIENT_ID'
client_secret: '$CLIENT_SECRET'
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: '$TOKEN_EXCHANGE_ENDPOINT_URL'
client_auth: client_secret_basic
client_id: '$EXCHANGE_CLIENT_ID'
client_secret: '$EXCHANGE_CLIENT_SECRET'
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
plugin: ai-mcp-oauth2
" | kubectl apply -f -
Prerequisite: Configure your Personal Access Token
terraform {
required_providers {
konnect = {
source = "kong/konnect"
}
}
}
provider "konnect" {
personal_access_token = "$KONNECT_TOKEN"
server_url = "https://us.api.konghq.com/"
}
Add the following to your Terraform configuration to create a Konnect Gateway Plugin:
resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = var.mcp_resource_url
authorization_servers = [var.authorization_server_url]
introspection_endpoint = var.introspection_endpoint_url
client_id = var.client_id
client_secret = var.client_secret
passthrough_credentials = true
token_exchange = {
enabled = true
token_endpoint = var.token_exchange_endpoint_url
client_auth = "client_secret_basic"
client_id = var.exchange_client_id
client_secret = var.exchange_client_secret
request = {
requested_token_type = "urn:ietf:params:oauth:token-type:access_token"
subject_token_type = "urn:ietf:params:oauth:token-type:access_token"
actor_token_source = "header"
actor_token_header = "X-Actor-Token"
scopes = ["mcp:read"]
}
cache = {
enabled = true
ttl = 3600
}
}
}
tags = []
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
}
This example requires the following variables to be added to your manifest. You can specify values at runtime by setting TF_VAR_name=value.
variable "exchange_client_secret" {
type = string
}
Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
service: serviceName|Id
config:
resource: ${{ env "DECK_MCP_RESOURCE_URL" }}
authorization_servers:
- ${{ env "DECK_AUTHORIZATION_SERVER_URL" }}
introspection_endpoint: ${{ env "DECK_INTROSPECTION_ENDPOINT_URL" }}
client_id: ${{ env "DECK_CLIENT_ID" }}
client_secret: ${{ env "DECK_CLIENT_SECRET" }}
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: ${{ env "DECK_TOKEN_EXCHANGE_ENDPOINT_URL" }}
client_auth: client_secret_basic
client_id: ${{ env "DECK_EXCHANGE_CLIENT_ID" }}
client_secret: ${{ env "DECK_EXCHANGE_CLIENT_SECRET" }}
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
Make sure to replace the following placeholders with your own values:
-
serviceName|Id: Theidornameof the service the plugin configuration will target.
Make the following request:
curl -i -X POST http://localhost:8001/services/{serviceName|Id}/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
Make sure to replace the following placeholders with your own values:
-
serviceName|Id: Theidornameof the service the plugin configuration will target.
Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/services/{serviceId}/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
Make sure to replace the following placeholders with your own values:
-
region: Geographic region where your Kong Konnect is hosted and operates. -
KONNECT_TOKEN: Your Personal Access Token (PAT) associated with your Konnect account. -
controlPlaneId: Theidof the control plane. -
serviceId: Theidof the service the plugin configuration will target.
See the Konnect API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ai-mcp-oauth2
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
konghq.com/tags: ''
config:
resource: '$MCP_RESOURCE_URL'
authorization_servers:
- '$AUTHORIZATION_SERVER_URL'
introspection_endpoint: '$INTROSPECTION_ENDPOINT_URL'
client_id: '$CLIENT_ID'
client_secret: '$CLIENT_SECRET'
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: '$TOKEN_EXCHANGE_ENDPOINT_URL'
client_auth: client_secret_basic
client_id: '$EXCHANGE_CLIENT_ID'
client_secret: '$EXCHANGE_CLIENT_SECRET'
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
plugin: ai-mcp-oauth2
" | kubectl apply -f -
Next, apply the KongPlugin resource by annotating the service resource:
kubectl annotate -n kong service SERVICE_NAME konghq.com/plugins=ai-mcp-oauth2
Prerequisite: Configure your Personal Access Token
terraform {
required_providers {
konnect = {
source = "kong/konnect"
}
}
}
provider "konnect" {
personal_access_token = "$KONNECT_TOKEN"
server_url = "https://us.api.konghq.com/"
}
Add the following to your Terraform configuration to create a Konnect Gateway Plugin:
resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = var.mcp_resource_url
authorization_servers = [var.authorization_server_url]
introspection_endpoint = var.introspection_endpoint_url
client_id = var.client_id
client_secret = var.client_secret
passthrough_credentials = true
token_exchange = {
enabled = true
token_endpoint = var.token_exchange_endpoint_url
client_auth = "client_secret_basic"
client_id = var.exchange_client_id
client_secret = var.exchange_client_secret
request = {
requested_token_type = "urn:ietf:params:oauth:token-type:access_token"
subject_token_type = "urn:ietf:params:oauth:token-type:access_token"
actor_token_source = "header"
actor_token_header = "X-Actor-Token"
scopes = ["mcp:read"]
}
cache = {
enabled = true
ttl = 3600
}
}
}
tags = []
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
service = {
id = konnect_gateway_service.my_service.id
}
}
This example requires the following variables to be added to your manifest. You can specify values at runtime by setting TF_VAR_name=value.
variable "exchange_client_secret" {
type = string
}
Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: ai-mcp-oauth2
route: routeName|Id
config:
resource: ${{ env "DECK_MCP_RESOURCE_URL" }}
authorization_servers:
- ${{ env "DECK_AUTHORIZATION_SERVER_URL" }}
introspection_endpoint: ${{ env "DECK_INTROSPECTION_ENDPOINT_URL" }}
client_id: ${{ env "DECK_CLIENT_ID" }}
client_secret: ${{ env "DECK_CLIENT_SECRET" }}
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: ${{ env "DECK_TOKEN_EXCHANGE_ENDPOINT_URL" }}
client_auth: client_secret_basic
client_id: ${{ env "DECK_EXCHANGE_CLIENT_ID" }}
client_secret: ${{ env "DECK_EXCHANGE_CLIENT_SECRET" }}
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
Make sure to replace the following placeholders with your own values:
-
routeName|Id: Theidornameof the route the plugin configuration will target.
Make the following request:
curl -i -X POST http://localhost:8001/routes/{routeName|Id}/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
Make sure to replace the following placeholders with your own values:
-
routeName|Id: Theidornameof the route the plugin configuration will target.
Make the following request:
curl -X POST https://{region}.api.konghq.com/v2/control-planes/{controlPlaneId}/core-entities/routes/{routeId}/plugins/ \
--header "accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $KONNECT_TOKEN" \
--data '
{
"name": "ai-mcp-oauth2",
"config": {
"resource": "'$MCP_RESOURCE_URL'",
"authorization_servers": [
"'$AUTHORIZATION_SERVER_URL'"
],
"introspection_endpoint": "'$INTROSPECTION_ENDPOINT_URL'",
"client_id": "'$CLIENT_ID'",
"client_secret": "'$CLIENT_SECRET'",
"passthrough_credentials": true,
"token_exchange": {
"enabled": true,
"token_endpoint": "'$TOKEN_EXCHANGE_ENDPOINT_URL'",
"client_auth": "client_secret_basic",
"client_id": "'$EXCHANGE_CLIENT_ID'",
"client_secret": "'$EXCHANGE_CLIENT_SECRET'",
"request": {
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"actor_token_source": "header",
"actor_token_header": "X-Actor-Token",
"scopes": [
"mcp:read"
]
},
"cache": {
"enabled": true,
"ttl": 3600
}
}
},
"tags": []
}
'
Make sure to replace the following placeholders with your own values:
-
region: Geographic region where your Kong Konnect is hosted and operates. -
KONNECT_TOKEN: Your Personal Access Token (PAT) associated with your Konnect account. -
controlPlaneId: Theidof the control plane. -
routeId: Theidof the route the plugin configuration will target.
See the Konnect API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: ai-mcp-oauth2
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
konghq.com/tags: ''
config:
resource: '$MCP_RESOURCE_URL'
authorization_servers:
- '$AUTHORIZATION_SERVER_URL'
introspection_endpoint: '$INTROSPECTION_ENDPOINT_URL'
client_id: '$CLIENT_ID'
client_secret: '$CLIENT_SECRET'
passthrough_credentials: true
token_exchange:
enabled: true
token_endpoint: '$TOKEN_EXCHANGE_ENDPOINT_URL'
client_auth: client_secret_basic
client_id: '$EXCHANGE_CLIENT_ID'
client_secret: '$EXCHANGE_CLIENT_SECRET'
request:
requested_token_type: urn:ietf:params:oauth:token-type:access_token
subject_token_type: urn:ietf:params:oauth:token-type:access_token
actor_token_source: header
actor_token_header: X-Actor-Token
scopes:
- mcp:read
cache:
enabled: true
ttl: 3600
plugin: ai-mcp-oauth2
" | kubectl apply -f -
Next, apply the KongPlugin resource by annotating the httproute or ingress resource:
kubectl annotate -n kong httproute konghq.com/plugins=ai-mcp-oauth2
kubectl annotate -n kong ingress konghq.com/plugins=ai-mcp-oauth2
Prerequisite: Configure your Personal Access Token
terraform {
required_providers {
konnect = {
source = "kong/konnect"
}
}
}
provider "konnect" {
personal_access_token = "$KONNECT_TOKEN"
server_url = "https://us.api.konghq.com/"
}
Add the following to your Terraform configuration to create a Konnect Gateway Plugin:
resource "konnect_gateway_plugin_ai_mcp_oauth2" "my_ai_mcp_oauth2" {
enabled = true
config = {
resource = var.mcp_resource_url
authorization_servers = [var.authorization_server_url]
introspection_endpoint = var.introspection_endpoint_url
client_id = var.client_id
client_secret = var.client_secret
passthrough_credentials = true
token_exchange = {
enabled = true
token_endpoint = var.token_exchange_endpoint_url
client_auth = "client_secret_basic"
client_id = var.exchange_client_id
client_secret = var.exchange_client_secret
request = {
requested_token_type = "urn:ietf:params:oauth:token-type:access_token"
subject_token_type = "urn:ietf:params:oauth:token-type:access_token"
actor_token_source = "header"
actor_token_header = "X-Actor-Token"
scopes = ["mcp:read"]
}
cache = {
enabled = true
ttl = 3600
}
}
}
tags = []
control_plane_id = konnect_gateway_control_plane.my_konnect_cp.id
route = {
id = konnect_gateway_route.my_route.id
}
}
This example requires the following variables to be added to your manifest. You can specify values at runtime by setting TF_VAR_name=value.
variable "exchange_client_secret" {
type = string
}