Skip to main content

Keycloak Integration

This walkthrough connects a Keycloak realm to the gateway end to end: users sign in to Keycloak as themselves, call the gateway with the resulting JWT, and run under a caller API key selected by their department or group — no per-user key distribution, no directory duplication. It is the concrete companion to JWT Authentication (trust and verification) and JWT Claim Mappings (evaluation semantics).

What you build:

  • A Keycloak realm whose access tokens carry the gateway audience, a department claim, and a groups claim.
  • Two policy keys at the gateway — a restricted finance key and an unrestricted platform key.
  • Two claim mappings: members of the platform-admin group run under the platform key; everyone with department = finance runs under the finance key; everyone else is rejected.

The steps were verified against Keycloak 26 (quay.io/keycloak/keycloak:26.3 start-dev). Field names may sit in slightly different places in other versions; the values are what matter.

Prerequisites

Before starting, prepare the following:

  • Administrator access to a Keycloak deployment.
  • A gateway proxy URL and two working model aliases named finance-model and general-model. The second model verifies that mapping priority selects the broader administrator key.
  • One gateway configuration path:
    • AISIX Cloud with an environment, an attached gateway, permission to manage trust providers, caller API keys, and claim mappings, and the model IDs for both test models.
    • An open-source AISIX gateway with an existing complete resources.yaml file that defines both test models and their provider keys, plus access to the gateway process environment.
  • curl and jq. The open-source AISIX gateway path also uses OpenSSL to generate placeholder caller-key values that agents never receive.

Create the Realm and Client

Create a realm (this walkthrough uses aisix) and an OpenID Connect client in it for the applications that will request tokens:

Client settingValue
Client IDai-clients
Client authenticationOn (confidential — the client has a secret)
Direct access grantsOn, if you want to test with curl password grants as below
Standard flowPer your application's sign-in shape

Note the client secret from the client's Credentials tab.

Add Claims to Access Tokens

Out of the box a Keycloak access token carries neither the gateway's audience nor your directory attributes. Add three protocol mappers to the client (Clients → ai-clients → Client scopes → ai-clients-dedicated → Add mapper → By configuration):

Mapper typeConfiguration
AudienceIncluded Custom Audience: aisix-gateway, add to access token. The gateway rejects tokens whose aud does not include a configured audience.
User AttributeUser Attribute: department, Token Claim Name: department, claim type String, add to access token.
Group MembershipToken Claim Name: groups, Full group path: Off, add to access token.

One realm-level switch matters on Keycloak 24 and later: custom user attributes such as department are silently dropped unless the realm's user profile knows them. Either declare the attribute (Realm settings → User profile → Create attribute) or set Unmanaged attributes to Enabled on the same screen. Without this, the attribute you type on a user simply does not persist, and the claim never appears in tokens.

Create Groups and Users

Create the platform-admin group, then the users:

  • alice: attribute department = finance.
  • bob: attribute department = finance and member of platform-admin, to prove that the group mapping wins when both mappings match.
  • charlie: neither, to prove the default-deny path.

Give every user a password and a complete profile (email, first name, last name — or relax the profile requirements in the realm's user profile). A user with unmet profile requirements or pending required actions fails a direct-grant login with Account is not fully set up.

Trust the Keycloak Realm

Register the realm as a trust provider. The issuer must equal the token's iss claim byte for byte — for a realm named aisix that is <keycloak base URL>/realms/aisix — and the JWKS URI is discovered from it automatically.

For the open-source AISIX gateway, add the provider to the complete resources file that already defines finance-model, general-model, and their provider keys. If corp-keycloak already exists from JWT Authentication, replace that entry instead of adding a second provider with the same name, and omit its bound_claims and required_scopes:

oidc_providers:
- name: corp-keycloak
issuer: https://sso.example.com/realms/aisix
audiences: ["aisix-gateway"]

In AISIX Cloud, create the same provider under Trust Providers or through the AISIX Cloud Admin API. Use the name, issuer, and audiences shown above. Omit bound_claims and required_scopes unless the tokens include those values; otherwise AISIX rejects the request with jwt_claims_rejected before claim mappings run. See JWT Authentication for the remaining provider fields.

Map Claims to Policy Keys

Create the two policy keys and the two mappings. The group rule gets the better (lower) priority so platform admins in the finance department still land on the platform key. Use the same configuration path you used to trust the realm.

AISIX Cloud

Create two caller API keys in the environment:

  • finance-policy-key, allowed to call only finance-model.
  • admin-policy-key, allowed to call finance-model, general-model, and any other models the administrators should reach.

Record both key IDs. Then open the environment's Claim mappings page and create these mappings, or submit the equivalent payloads through the AISIX Cloud Admin API workflow:

MappingPriorityConditionPolicy key
platform-admins50groups contains platform-adminadmin-policy-key
finance-dept100department exactly matches financefinance-policy-key

Open-Source AISIX Gateway

Generate values for the two caller-key resources and set them in the gateway process environment. The users authenticate with Keycloak tokens, so do not distribute these values to them:

export FINANCE_POLICY_KEY="$(openssl rand -hex 32)"
export ADMIN_POLICY_KEY="$(openssl rand -hex 32)"

Add the two policy keys to api_keys and the two mappings to claim_mappings. Create claim_mappings if needed, and keep the other resources unchanged:

resources.yaml (API keys and claim mappings)
api_keys:
- display_name: finance-policy-key
key_env: FINANCE_POLICY_KEY
allowed_models: ["finance-model"]
- display_name: admin-policy-key
key_env: ADMIN_POLICY_KEY
allowed_models: ["*"]

claim_mappings:
- name: platform-admins
jwt_provider: corp-keycloak
priority: 50
match:
- claim: groups
op: contains
values: ["platform-admin"]
resolve:
api_key: admin-policy-key
- name: finance-dept
jwt_provider: corp-keycloak
priority: 100
match:
- claim: department
op: exact
values: ["finance"]
resolve:
api_key: finance-policy-key

Validate and apply the complete resources file by following Add New Environment Variables. Exporting the variables on the host does not add them to an already-running container, so recreate the gateway with both variables instead of sending only SIGHUP.

contains is the operator for Keycloak's array claims (groups here, or realm_access.roles for realm roles — dots traverse nested objects); exact fits single-valued attributes like department. Evaluation order, tie-breaking, and the interaction with direct jwt_subject bindings are specified in JWT Claim Mappings.

Verify Claim Mapping

Export the Keycloak client, user, gateway, and model values. The Keycloak URL and gateway URL have no trailing slash:

export KEYCLOAK_URL="https://sso.example.com"
export KEYCLOAK_CLIENT_ID="ai-clients"
export KEYCLOAK_CLIENT_SECRET="YOUR_CLIENT_SECRET"
export KEYCLOAK_USERNAME="alice"
export KEYCLOAK_PASSWORD="ALICE_PASSWORD"
export AISIX_PROXY="https://gateway.example.com"
export AISIX_MODEL="finance-model"

Fetch the user's token and call the gateway with it as the bearer. URL encoding preserves client secrets and passwords that contain form-delimiter characters:

TOKEN=$(curl -sS --fail-with-body \
"$KEYCLOAK_URL/realms/aisix/protocol/openid-connect/token" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=$KEYCLOAK_CLIENT_ID" \
--data-urlencode "client_secret=$KEYCLOAK_CLIENT_SECRET" \
--data-urlencode "username=$KEYCLOAK_USERNAME" \
--data-urlencode "password=$KEYCLOAK_PASSWORD" \
| jq -er '.access_token')

curl -sS --fail-with-body "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"model": "${AISIX_MODEL}",
"messages": [
{
"role": "user",
"content": "hello"
}
]
}
EOF

Repeat the token and gateway requests after setting KEYCLOAK_USERNAME, KEYCLOAK_PASSWORD, and AISIX_MODEL for each matrix row. Testing another model requires another configured model alias.

For the expected 401 and 403 rows, curl exits with a nonzero status because --fail-with-body is set, but it still prints the gateway's error body.

The three users pin the whole contract:

CallerRequestResult
alice (department = finance)finance-modelSucceeds. The finance-dept mapping selected the finance key.
alicegeneral-modelReturns 403. The finance key's allowed_models governs every request it admits.
bob (finance department and platform-admin group)general-modelSucceeds. The group rule at priority 50 outranks the matching finance rule at priority 100.
charlie (no match)finance-modelReturns 401 with code jwt_identity_unmapped. No rule matched, and there is no default admission.

Every request's usage is attributed to the individual: usage events and logs carry jwt_subject (Keycloak's sub by default), the provider name, and the matched mapping — see Attribution.

Troubleshooting

SymptomCause and fix
Token endpoint answers Account is not fully set upThe user has pending required actions or an incomplete profile for a direct grant. Complete email/first/last name (or relax the realm's user profile requirements) and clear required actions.
department never appears in the tokenOn Keycloak 24+ the user profile drops undeclared attributes. Declare the attribute or enable unmanaged attributes as described in Add Claims to Access Tokens, then re-set the value on the user.
401 with jwt_invalid despite a fresh tokenThe token's aud does not include a configured audience (missing audience mapper), or issuer does not equal the token's iss exactly — scheme, host, and path included.
401 with jwt_identity_unmappedVerification succeeded but no mapping matched: the claim is missing from the token, the operator does not fit the claim's type (exact on an array, contains on a string), or no rule covers the identity. Decode the token (for example at jwt.io) and compare its claims against your match conditions.