1
 
 
Account
In your account you can view the status of your application, save incomplete applications and view current news and events
May 13, 2026

Secure Service Tokens with Keycloak Token
Exchange – Automated with Terraform

What is this article about?

In this article, you’ll learn how to make service-to-service communication in microservice architectures more secure using Keycloak Token Exchange. It demonstrates how to generate service-specific tokens with the appropriate audience and limited permissions according to the OAuth 2.0 standard — and how the necessary Keycloak configuration can be automated reproducibly with Terraform.

In modern software development, shaped by microservices and distributed systems, secure authentication and authorization for service-to-service communication are critical. This introduction shows how to secure service tokens using Keycloak’s Token Exchange feature and how to manage the required infrastructure efficiently with Terraform. Keycloak acts as a central identity and access management solution, enabling identity management and orchestrating token exchange for access to protected resources. Automating these complex configurations with Terraform ensures consistency and reproducibility while also supporting agile deployment and maintenance of the security infrastructure.

Token exchange explained

A token is a digital security certificate that contains the electronic keys required for
authentication and authorization. These keys define the rights and access permissions granted to the user, thereby enabling secure access to digital systems.

Token exchange involves replacing an existing token with general permissions with a new token that is specifically tailored to the requirements of a specific application or service. This aligns with the "need-to-know" principle, as the new token contains only the permissions required for the respective service. If this service is compromised, the new token cannot be used for authorization in other applications or services. The token exchange takes place via a trusted client without requiring any user interaction.

Introduction

As the central infrastructure team, we provide Keycloak to our development teams to support secure and efficient application development in the context of authentication and authorization. Keycloak handles centralized user identity management. Within Keycloak, we provide each development team with an isolated environment known as a “realm.” In this realm, users, applications (clients), roles, and all associated security and authentication data are managed separately.

Standard Token Exchange (RFC 8693)

The Token Exchange feature has been available in Keycloak for some time, but until now it has only been available as a preview feature. With the release of Keycloak 26.2, the standard Token Exchange is now officially supported and is fully compliant with the OAuth 2.0 Token Exchange (RFC 8693). Below, I’ll show how the process can be set up with Terraform and executed via a script. (This example demonstrates how Keycloak configurations can be implemented automatically and reproducibly with Terraform. You can find more fundamentals and best practices on this topic in our Terraform series  “Scalable & Reliable Infrastructure-as-Code”)

In this example, we’ll start a local Keycloak Docker container. A bootstrap script creates a client realm named "web-app" with the client "terraform_client." This client allows our clients to fully configure their realm using Terraform.

The following example shows an id_token issued by Keycloak after a successful login, and an access_token used to authorize access to APIs or protected resources.


id_token:
{
  "exp": 1763711539,
  "iat": 1763711239,
  "jti": "cdbd39ba-f49d-4dbe-98e9-17180f1ededb",
  "iss": "http://localhost:8080/realms/web-app",
  "aud": "frontend",
  "sub": "f817130a-3eea-4c7a-82cd-294d7c29810a",
  "typ": "ID",
  "azp": "frontend",
  "sid": "5d6e3640-8ed6-41e8-896d-415b0a4a949d",
  "at_hash": "4MUB7cfW_PdsSP2Bj0Y3Lg"
}

access_token:
{
  "exp": 1763711539,
  "iat": 1763711239,
  "jti": "onrtro:eb92b411-1619-45c0-91a4-2c2d6a681f26",
  "iss": "http://localhost:8080/realms/web-app",
  "aud": "api_gateway",
  "typ": "Bearer",
  "azp": "frontend",
  "sid": "5d6e3640-8ed6-41e8-896d-415b0a4a949d",
  "scope": "openid audience-scope"
}


Keycloak makes it possible to specify an audience ("aud") in the token payload (claims), in addition to the issuer ("iss"), the user ("sub"), and the expiration time ("exp").

This audience determines which application the token is intended for. The token is therefore only valid for the service that matches the specified audience and cannot be misused for other services.

To demonstrate the Token Exchange principle, the following clients are configured in the realm: a frontend client, an API gateway client, and three service clients (Booking, Customer, and Order Service). In addition, an Admin User is created with the corresponding permissions for all client services.

Frontend client

The frontend client is configured as PUBLIC, which means that no client secret is required for authentication. Full scope is disabled because the required permissions are assigned exclusively via the audience:

resource "keycloak_openid_client" "frontend" {
    realm_id                     = data.keycloak_realm.web_app.id
    client_id                    = "frontend"
    name                         = "Frontend"
    access_type                  = "PUBLIC"
    direct_access_grants_enabled = true
    full_scope_allowed           = false
}

resource "keycloak_openid_audience_protocol_mapper" "audience_mapper" {
    realm_id                = data.keycloak_realm.web_app.id
    client_id               = keycloak_openid_client.frontend.id
    name                     = "audience-mapper-api"
    included_custom_audience = keycloak_openid_client.api_gateway.name
    add_to_access_token      = true
    add_to_id_token          = false
}

API gateway client

The API gateway client is configured as confidential (CONFIDENTIAL) and uses the new Standard Token Exchange feature. This means that authentication requires both the client_id and the client_secret.

resource "keycloak_openid_client" "api_gateway" {
  realm_id                     = data.keycloak_realm.web_app.id
  name                         = "api gateway"
  client_id                    = "api_gateway"
  client_secret                = "secret"
  description                  = "A API gateway client on the
destination realm"

  access_type                  = "CONFIDENTIAL"
  standard_token_exchange_enabled = true
}


All service clients are configured identically as confidential clients through the Terraform module named service.

Admin user

For the demo script, an admin user is created and assigned all roles that have been configured:

resource "keycloak_user_roles" "user_roles" {
  realm_id   = data.keycloak_realm.web_app.id
  user_id    = keycloak_user.admin_user.id

  role_ids = concat(
    [for s in local.services : module.service[s.name].service_role_id],
    [keycloak_role.frontendAdmin.id]
  )
}

Demo script

Get frontend token

An access token for the user admin with password is requested by the frontend
client from the Keycloak server:

TOKEN_FRONTEND=$(curl -s $KC_URL \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'username=admin' \
  -d 'password=realmAdmin' \
  -d 'grant_type=password' \
  -d 'client_id=frontend' \
  -d 'scope=openid' )


The received token is decoded and relevant fields are displayed:

{
  "sub": null,
  "azp": "frontend",
  "aud": "api_gateway",
  "resource_access": null,
  "scope": "openid audience-scope"
}


The access token contains azp (Authorized Party); sub, as the user’s unique ID, is no
longer included by default to keep the token smaller and more efficient. aud
(audience) is only api_gateway here, as full scope was not permitted.

Token Exchange for the Customer Service

The frontend token is exchanged for a new token with the Customer Service audience using the webapp_client.

TOKEN_EXCHANGE=$(curl -s $KC_URL \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "api_gateway:secret" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=$TOKEN_FRONTEND" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "audience=CustomerService" | jq -r .access_token)

Access Token

{
  "sub": "0384647c-51d7-4999-a534-cb09c67dda1d",
  "azp": "api_gateway",
  "aud": "CustomerService",
  "resource_access": {
    "CustomerService": {
      "roles": [
        "write"
      ]
    }
  },
  "scope": "profile email"
}


The exchange token now includes the audience CustomerService and only grants write access to that service. With "sub," it can be uniquely assigned to a user.

Token Exchange for the Booking Service

The frontend token is exchanged for a new token with the audience Booking Service
using the webapp_client.

TOKEN_EXCHANGE=$(curl -s $KC_URL \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "api_gateway:secret" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=$TOKEN_FRONTEND" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "audience=BookingService" | jq -r .access_token)


In this example, BookingService is the audience, which enables write access to this
service.

{
  "sub": "0384647c-51d7-4999-a534-cb09c67dda1d",
  "azp": "api_gateway",
  "aud": "BookingService",
  "resource_access": {
    "BookingService": {
      "roles": [
        "write"
      ]
    }
  },
  "scope": "profile email"
}

Conclusion

The current Keycloak Terraform provider, which is updated regularly, makes it easy to implement new features such as Standard Token Exchange. Token exchange provides important security benefits: it replaces a token with broad permissions with a service-specific token that contains only the permissions required by the target service (need-to-know principle). This reduces the potential impact of a compromise, as the token cannot be reused for other applications or services.

We welcome feedback and suggestions in the comments. ⬇️

And if you’d like to learn more about secure infrastructure and the protection of sensitive information, we also recommend taking a look at our article “Securely Managing Sensitive Data with HashiCorp Vault.”

Want to work with us?

0No comments yet.
Write a comment
Leave us a comment here and let the authors know what you thought of the article.
Answer to: Reply directly to the topic

Written by

Andreas Nacke
Andreas Nacke
Senior IT Administrator

Similar Articles

Saved!

We want to improve out content with your feedback.

How interesting is this blogpost?

We have received your feedback.