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
departmentclaim, and agroupsclaim. - Two policy keys at the gateway — a restricted
financekey and an unrestricted platform key. - Two claim mappings: members of the
platform-admingroup run under the platform key; everyone withdepartment = financeruns 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-modelandgeneral-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.yamlfile that defines both test models and their provider keys, plus access to the gateway process environment.
curlandjq. 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 setting | Value |
|---|---|
| Client ID | ai-clients |
| Client authentication | On (confidential — the client has a secret) |
| Direct access grants | On, if you want to test with curl password grants as below |
| Standard flow | Per 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 type | Configuration |
|---|---|
| Audience | Included Custom Audience: aisix-gateway, add to access token. The gateway rejects tokens whose aud does not include a configured audience. |
| User Attribute | User Attribute: department, Token Claim Name: department, claim type String, add to access token. |
| Group Membership | Token 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: attributedepartment = finance.bob: attributedepartment = financeand member ofplatform-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 onlyfinance-model.admin-policy-key, allowed to callfinance-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:
| Mapping | Priority | Condition | Policy key |
|---|---|---|---|
platform-admins | 50 | groups contains platform-admin | admin-policy-key |
finance-dept | 100 | department exactly matches finance | finance-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:
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:
| Caller | Request | Result |
|---|---|---|
alice (department = finance) | finance-model | Succeeds. The finance-dept mapping selected the finance key. |
alice | general-model | Returns 403. The finance key's allowed_models governs every request it admits. |
bob (finance department and platform-admin group) | general-model | Succeeds. The group rule at priority 50 outranks the matching finance rule at priority 100. |
charlie (no match) | finance-model | Returns 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
| Symptom | Cause and fix |
|---|---|
Token endpoint answers Account is not fully set up | The 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 token | On 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 token | The 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_unmapped | Verification 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. |