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.
Step 1: 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.
Step 2: Shape the Access Token
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.
Step 3: Create Groups and Users
Create the platform-admin group, then the users:
alice— attributedepartment = finance.bob— member ofplatform-admin.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.
Step 4: Trust the Realm at the Gateway
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.
In a resources file:
oidc_providers:
- name: corp-keycloak
issuer: https://sso.example.com/realms/aisix
audiences: ["aisix-gateway"]
On a managed deployment, create the same provider under Trust Providers in the dashboard, or via the Admin API — the fields are identical (see JWT Authentication).
Step 5: Map Departments and Groups 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:
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
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.
Step 6: Verify
Fetch a token for each user and call the gateway with it as the bearer:
TOKEN=$(curl -s "https://sso.example.com/realms/aisix/protocol/openid-connect/token" \
-d grant_type=password -d client_id=ai-clients -d client_secret="$CLIENT_SECRET" \
-d username=alice -d password="$ALICE_PASSWORD" | jq -r .access_token)
curl "https://gateway.example.com/v1/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "finance-model", "messages": [{"role": "user", "content": "hello"}]}'
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 | any other model | 403 — the finance key's allowed_models governs every request it admits. |
bob (platform-admin group) | any model | Succeeds — the group rule outranks everything at priority 50. |
charlie (no match) | any model | 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 (Step 2), 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. |