Token exchange with an actor tokenv3.16+
Use the OpenID Connect plugin to include an actor token in a token exchange request, representing the identity of the party acting on behalf of the subject. This is useful for delegation scenarios, such as a backend service or AI agent acting on behalf of a user, and is required by identity providers (such as Okta) that expect an actor token during certain token exchange grants.
In this example, Kong Gateway reads the actor token from a request header and includes it in the token exchange request along with the subject token.
Here’s how token exchange works with the OIDC plugin:
sequenceDiagram
participant C as Client
(e.g. mobile app)
participant K as API Gateway
with OIDC plugin
participant A as Authorization server
(e.g. Keycloak)
participant U as Upstream
(backend service,
e.g. httpbin)
C->>K: Request with subject token
activate K
note over K: Validate subject token
(iss, exp, nbf)
K->>A: Token exchange request
activate A
A-->>K: Exchanged access token
deactivate A
K->>K: Validate exchanged token
K->>U: Proxy request with exchanged token
activate U
U-->>K: Response
deactivate U
K-->>C: Response
deactivate K
For more detail on token exchange support, see the Token exchange reference.
Prerequisites
- An identity provider that supports token exchange and accepts an actor token during the exchange.
Environment variables
-
ISSUER: The issuer authentication URL for the authorization server that issued the token in the incoming request. For example, if you’re using Keycloak as your IdP, the issuer URL looks like this:http://localhost:8080/realms/example-realm. -
CLIENT_ID: The client ID that the plugin uses when it calls authenticated endpoints of the IdP. -
CLIENT_SECRET: The client secret needed to connect to your IdP. -
ACTOR_TOKEN_HEADER: The name of the request header that carries the actor token, for exampleX-Actor-Token. The client or an upstream service must set this header on the incoming request for Kong Gateway to include it in the exchange.
Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: openid-connect
config:
issuer: ${{ env "DECK_ISSUER" }}
client_id:
- ${{ env "DECK_CLIENT_ID" }}
client_secret:
- ${{ env "DECK_CLIENT_SECRET" }}
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: ${{ env "DECK_ISSUER" }}
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: ${{ env "DECK_ACTOR_TOKEN_HEADER" }}
type: urn:ietf:params:oauth:token-type:access_tokenMake the following request:
curl -i -X POST http://localhost:8001/plugins/ \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data '
{
"name": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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 Control Planes Config API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongClusterPlugin
metadata:
name: openid-connect
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
labels:
global: 'true'
config:
issuer: '$ISSUER'
client_id:
- '$CLIENT_ID'
client_secret:
- '$CLIENT_SECRET'
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: '$ISSUER'
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: '$ACTOR_TOKEN_HEADER'
type: urn:ietf:params:oauth:token-type:access_token
plugin: openid-connect
" | 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_openid_connect" "my_openid_connect" {
enabled = true
config = {
issuer = var.issuer
client_id = [var.client_id]
client_secret = [var.client_secret]
client_auth = ["client_secret_post"]
auth_methods = ["bearer"]
token_exchange = {
subject_token_issuers = [
{
issuer = var.issuer
conditions = {
missing_audience =
has_audience =
missing_scopes = ["profile"]
has_scopes =
}
} ]
request = {
empty_audience = false
scopes =
empty_scopes = false
audience =
actor_token = {
source = "header"
header_name = var.actor_token_header
type = "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
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 "issuer" {
type = string
}
variable "client_id" {
type = string
}
variable "client_secret" {
type = string
}
variable "actor_token_header" {
type = string
}Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: openid-connect
service: serviceName|Id
config:
issuer: ${{ env "DECK_ISSUER" }}
client_id:
- ${{ env "DECK_CLIENT_ID" }}
client_secret:
- ${{ env "DECK_CLIENT_SECRET" }}
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: ${{ env "DECK_ISSUER" }}
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: ${{ env "DECK_ACTOR_TOKEN_HEADER" }}
type: urn:ietf:params:oauth:token-type:access_tokenMake 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": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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 Control Planes Config API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: openid-connect
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
config:
issuer: '$ISSUER'
client_id:
- '$CLIENT_ID'
client_secret:
- '$CLIENT_SECRET'
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: '$ISSUER'
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: '$ACTOR_TOKEN_HEADER'
type: urn:ietf:params:oauth:token-type:access_token
plugin: openid-connect
" | kubectl apply -f -Next, apply the KongPlugin resource by annotating the service resource:
kubectl annotate -n kong service SERVICE_NAME konghq.com/plugins=openid-connectPrerequisite: 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_openid_connect" "my_openid_connect" {
enabled = true
config = {
issuer = var.issuer
client_id = [var.client_id]
client_secret = [var.client_secret]
client_auth = ["client_secret_post"]
auth_methods = ["bearer"]
token_exchange = {
subject_token_issuers = [
{
issuer = var.issuer
conditions = {
missing_audience =
has_audience =
missing_scopes = ["profile"]
has_scopes =
}
} ]
request = {
empty_audience = false
scopes =
empty_scopes = false
audience =
actor_token = {
source = "header"
header_name = var.actor_token_header
type = "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
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 "issuer" {
type = string
}
variable "client_id" {
type = string
}
variable "client_secret" {
type = string
}
variable "actor_token_header" {
type = string
}Add this section to your kong.yaml configuration file:
_format_version: "3.0"
plugins:
- name: openid-connect
route: routeName|Id
config:
issuer: ${{ env "DECK_ISSUER" }}
client_id:
- ${{ env "DECK_CLIENT_ID" }}
client_secret:
- ${{ env "DECK_CLIENT_SECRET" }}
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: ${{ env "DECK_ISSUER" }}
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: ${{ env "DECK_ACTOR_TOKEN_HEADER" }}
type: urn:ietf:params:oauth:token-type:access_tokenMake 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": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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": "openid-connect",
"config": {
"issuer": "'$ISSUER'",
"client_id": [
"'$CLIENT_ID'"
],
"client_secret": [
"'$CLIENT_SECRET'"
],
"client_auth": [
"client_secret_post"
],
"auth_methods": [
"bearer"
],
"token_exchange": {
"subject_token_issuers": [
{
"issuer": "'$ISSUER'",
"conditions": {
"missing_audience": null,
"has_audience": null,
"missing_scopes": [
"profile"
],
"has_scopes": null
}
}
],
"request": {
"empty_audience": false,
"scopes": null,
"empty_scopes": false,
"audience": null,
"actor_token": {
"source": "header",
"header_name": "'$ACTOR_TOKEN_HEADER'",
"type": "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
}
'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 Control Planes Config API reference to learn about region-specific URLs and personal access tokens.
echo "
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: openid-connect
namespace: kong
annotations:
kubernetes.io/ingress.class: kong
config:
issuer: '$ISSUER'
client_id:
- '$CLIENT_ID'
client_secret:
- '$CLIENT_SECRET'
client_auth:
- client_secret_post
auth_methods:
- bearer
token_exchange:
subject_token_issuers:
- issuer: '$ISSUER'
conditions:
missing_audience:
has_audience:
missing_scopes:
- profile
has_scopes:
request:
empty_audience: false
scopes:
empty_scopes: false
audience:
actor_token:
source: header
header_name: '$ACTOR_TOKEN_HEADER'
type: urn:ietf:params:oauth:token-type:access_token
plugin: openid-connect
" | kubectl apply -f -Next, apply the KongPlugin resource by annotating the httproute or ingress resource:
kubectl annotate -n kong httproute konghq.com/plugins=openid-connectkubectl annotate -n kong ingress konghq.com/plugins=openid-connectPrerequisite: 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_openid_connect" "my_openid_connect" {
enabled = true
config = {
issuer = var.issuer
client_id = [var.client_id]
client_secret = [var.client_secret]
client_auth = ["client_secret_post"]
auth_methods = ["bearer"]
token_exchange = {
subject_token_issuers = [
{
issuer = var.issuer
conditions = {
missing_audience =
has_audience =
missing_scopes = ["profile"]
has_scopes =
}
} ]
request = {
empty_audience = false
scopes =
empty_scopes = false
audience =
actor_token = {
source = "header"
header_name = var.actor_token_header
type = "urn:ietf:params:oauth:token-type:access_token"
}
}
}
}
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 "issuer" {
type = string
}
variable "client_id" {
type = string
}
variable "client_secret" {
type = string
}
variable "actor_token_header" {
type = string
}