Limited Time Offer!
For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!
What you’ll learn
- The purpose of Client Scopes and how mappers work.
- When to use Default vs Optional client scopes.
- How to map user properties, user attributes, roles, and groups into OIDC tokens.
- How to build nested JSON claims, scripted claims, and hardcoded claims.
- How to test with the Authorization Code + PKCE flow and validate claims.
- Best practices, security notes, troubleshooting, and CLI automation.
Applies to: Keycloak 20+ (and newer). UI labels may differ slightly across versions, but concepts and steps remain the same.
Quick mental model
- Client = An application that asks for tokens.
- Client Scope = A reusable bundle of mappers.
- Mapper = A rule that maps information (from Keycloak’s user model/roles/groups/etc.) into token claims.
- Claim = A field in the ID Token / Access Token / UserInfo response.
flowchart LR
A[User authenticates] --> B[Client requests tokens]
B --> C[Keycloak]
C --> D[Evaluate Client Scopes]
D --> E[Run Mappers]
E --> F[Produce Claims in Tokens]
F --> G[Application consumes tokens]
Prerequisites
- A Keycloak realm (e.g.,
wizbrand
). - An OIDC confidential or public client (e.g.,
wizbrand-web
). - A test user who can log in.
- Ability to reach the realm’s Authorization Endpoint and Token Endpoint (for testing).
Tip: Use a dedicated dev/test realm so you can experiment safely.
Core concepts
Client Scopes
- Default Client Scopes: Automatically applied to a client’s token requests.
- Optional Client Scopes: Only applied when explicitly requested via the
scope
parameter in the OIDC request (e.g.,scope=openid profile email my-optional-scope
).
Mappers (OIDC protocol)
Common mapper types you’ll use:
- User Property — maps built‑in user fields (username, email, first/last name).
- User Attribute — maps custom attributes you add to users (e.g.,
department
,tenantId
). - Group Membership — adds user’s groups as a claim (optionally with full path).
- Role mappers — project roles into custom claims (beyond the default
realm_access
/resource_access
). - Script Mapper — generate claims with custom logic.
- Hardcoded claim — inject a fixed value (useful for markers/flags).
Where claims appear
- ID Token: for the client (frontend) to know who the user is.
- Access Token: for APIs to authorize requests.
- UserInfo: fetched via the UserInfo endpoint using the Access Token.
Only enable claims in the places you actually need them.
Lab 0 — One‑time setup
Goal: A working client and user to test with.
- Create/verify realm: Realm name:
wizbrand
. - Create client
wizbrand-web
:- Protocol: OpenID Connect
- Access type: Public (for SPA) or Confidential (for server/web app)
- Valid redirect URIs:
http://localhost:3000/*
,http://localhost:8080/*
(adjust for your app) - Standard flow: ON (Authorization Code)
- PKCE: ON for public/SPA
- Create user
rajesh
with a password, verify Email Verified if you plan to exposeemail
claim.
If using a confidential client, note the client secret for token requests.
Lab 1 — Create a reusable Client Scope with basic profile claims
Goal: Build a scope that adds core identity fields in a standard way.
- Create Client Scope
- Name:
app-common
- Protocol: OpenID Connect
- Name:
- Add mappers
- User Property → email
- Token Claim Name:
email
- Include in ID token: ✅ Include in Access token: ➖ (only if your APIs need it)
- Add to userinfo: ✅
- Token Claim Name:
- User Property → given_name(first name)
- Token Claim Name:
given_name
- ID token ✅, UserInfo ✅
- Token Claim Name:
- User Property → family_name(last name)
- Token Claim Name:
family_name
- ID token ✅, UserInfo ✅
- Token Claim Name:
- User Property → email
You could alternatively attach Keycloak’s built‑in
profile
/
- Attach the scope to the client
- Client → Client scopes → Add as Default → select
app-common
.
- Client → Client scopes → Add as Default → select
- Test
- Log in via your app or use a test tool.
- Decode the ID Token at https://jwt.io (offline) or inspect in your app.
- You should see:
{
"email": "rajesh@example.com",
"given_name": "Rajesh",
"family_name": "Kumar"
}
Lab 2 — Add Groups to tokens (RBAC via groups)
Goal: Add groups
claim to tokens for group‑based authorization.
- Create groups
hospital
doctor
patient
Optionally nest them:/wizbrand/hospital
,/wizbrand/doctor
,/wizbrand/patient
.
- Assign user
rajesh
to one or more groups. - Add Group Membership mapper to
app-common
- Mapper Type: Group Membership
- Token Claim Name:
groups
- Full group path: ON (so claim shows
/wizbrand/hospital
instead of justhospital
) - ID token: ✅ Access token: ✅ UserInfo: ✅
- Test
Expect something like:
{
"groups": [
"/wizbrand/hospital"
]
}
Tip: Many libraries expect groups in a flat string array. Keep the claim as
String
type. If you don’t want the full path, turn that option OFF.
Lab 3 — Expose roles in a convenient claim
Goal: Make roles easy for your app/API to consume.
By default, roles appear under:
{
"realm_access": { "roles": ["admin", "user"] },
"resource_access": {
"wizbrand-web": { "roles": ["writer", "reader"] }
}
}
If your app prefers a flat claim (e.g., roles: ["admin","writer"]
), add a mapper.
- Create a new mapper in
app-common
- Mapper Type: Role name mapper (or equivalent “Aggregate realm/client roles into claim” in your KC version)
- Token Claim Name:
roles
- Claim JSON Type:
String
(KC will make an array) - Realm roles and Client roles: Include as per your need
- ID token: ➖ Access token: ✅ (APIs usually need roles)
- Test
{
"roles": ["admin","writer"]
}
Note: If you’re happy to consume
realm_access
/resource_access
directly, you can skip this mapper.
Lab 4 — Custom user attributes (department, tenant, tier)
Goal: Add business metadata to tokens.
- Add attributes to user
rajesh
:department = Finance
tenantId = wiz-001
tier = pro
- Add mappers (to
app-common
)- Mapper Type: User Attribute
- User Attribute:
department
- Token Claim Name:
app.department
- Claim JSON Type:
String
- ID token ✅, Access token ✅
tenantId
→app.tenantId
, andtier
→app.tier
. - Result (KC uses dotted claim names to build nested JSON):
{
"app": {
"department": "Finance",
"tenantId": "wiz-001",
"tier": "pro"
}
}
Tip: Namespacing your custom claims (e.g., under
app.*
orhttps://wizbrand.com/claims/*
) avoids collisions with standard OIDC fields.
Lab 5 — Scripted claim (dynamic logic)
Goal: Compute a claim based on conditions.
- Add a Script Mapper to
app-common
- Token Claim Name:
app.is_admin
- Script (example):
- Token Claim Name:
// available: user, realm, clientSession, session
// returns the value to place in the claim
var hasAdmin = user.getRoleMappings().stream()
.anyMatch(function(r) { return r.getName() === 'admin'; });
hasAdmin
- Claim JSON Type:
boolean
- ID token ➖, Access token ✅
- Test
{
"app": {
"is_admin": true
}
}
Caution: Script mappers run on the auth path. They add latency and can impact throughput. Prefer static mappers when possible.
Lab 6 — Hardcoded claim (markers/flags)
Goal: Stamp a fixed marker into tokens.
- Add Hardcoded Claim to
app-common
- Token Claim Name:
app.license
- Claim value:
enterprise
- Claim JSON Type:
String
- ID token ✅, Access token ✅
- Token Claim Name:
Result
{
"app": { "license": "enterprise" }
}
Attaching scopes: Default vs Optional
- Default: Always applied to the client (no work from the caller).
- Optional: Only applied when requested with
scope
.
Example OIDC request (Authorization Code + PKCE):
GET /realms/wizbrand/protocol/openid-connect/auth
?client_id=wizbrand-web
&response_type=code
&redirect_uri=http://localhost:8080/callback
&scope=openid app-common app-advanced
&code_challenge=...
&code_challenge_method=S256
If app-advanced
is an Optional client scope, it will be included only when present in scope
.
Test the tokens end‑to‑end
1) Get an authorization code
Use your app or a tool (like OAuth 2.0 client in Postman) to perform the Authorization Code + PKCE flow.
2) Exchange code for tokens
POST /realms/wizbrand/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
client_id=wizbrand-web
&grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=http://localhost:8080/callback
&code_verifier=CODE_VERIFIER
3) Inspect tokens
- Decode ID Token and Access Token (JWT) at jwt.io (offline), or
- Call UserInfo with the Access Token:
GET /realms/wizbrand/protocol/openid-connect/userinfo
Authorization: Bearer ACCESS_TOKEN
Confirm your claims are present where you expect them.
Automate with kcadm.sh
(CLI)
Run from Keycloak’s
bin/
directory or whereverkcadm.sh
is available.
- Login
kcadm.sh config credentials --server http://localhost:8080/auth \
--realm master --user admin --password 'admin'
- Create client scope
kcadm.sh create client-scopes -r wizbrand -s name=app-common -s protocol=openid-connect
- Add User Attribute mapper (department → app.department)
SCOPE_ID=$(kcadm.sh get client-scopes -r wizbrand -q name=app-common --fields id --format csv | tail -n1)
kcadm.sh create protocol-mappers/models -r wizbrand \
-s name=department \
-s protocol=openid-connect \
-s protocolMapper=oidc-usermodel-attribute-mapper \
-s 'config."user.attribute"=department' \
-s 'config."claim.name"=app.department' \
-s 'config."jsonType.label"=String' \
-s 'config."id.token.claim"=true' \
-s 'config."access.token.claim"=true' \
-s 'config."userinfo.token.claim"=true' \
-r wizbrand client-scopes/$SCOPE_ID/protocol-mappers/models
- Attach as Default to client
CLIENT_ID=$(kcadm.sh get clients -r wizbrand -q clientId=wizbrand-web --fields id --format csv | tail -n1)
kcadm.sh update clients/$CLIENT_ID/default-client-scopes/$SCOPE_ID -r wizbrand
Similar commands can create group/role/script/hardcoded mappers. Use the appropriate
protocolMapper
ids (e.g.,oidc-group-membership-mapper
,oidc-hardcoded-claim-mapper
,oidc-script-based-protocol-mapper
).
Best practices checklist
- Minimize PII and sensitive data in tokens; prefer UserInfo for sensitive fields.
- Keep tokens small (< 8 KB). Large tokens cause header bloat and gateway/proxy issues.
- Namespace custom claims (
app.*
orhttps://your-domain/claims/*
). - Prefer Default scopes for must‑have claims; Optional scopes for sometimes‑needed data.
- Enable only where needed (ID token vs Access token vs UserInfo) to reduce exposure.
- Avoid heavy Script mappers on high‑traffic realms; precompute attributes where possible.
- Rotate keys and validate JWT signatures in apps/APIs.
- Cache aware: Tokens won’t reflect user changes until the next login/refresh by default.
Troubleshooting
- My claim doesn’t show up
- Is the mapper in a scope that’s attached to the client?
- If it’s Optional, did you request it via
scope
? - Did you enable ID token / Access token / UserInfo in the mapper?
- Does the user actually have the attribute/role/group?
- Is Claim JSON Type correct (e.g.,
boolean
vsString
)? - Are you decoding the right token (ID vs Access)?
- Did you log out or refresh token after changes?
- Groups appear with slashes (e.g.,
/wizbrand/hospital
)- Disable Full group path if you want just the leaf names.
- APIs don’t see claims
- Ensure the claim is in the Access Token, not only in the ID Token.
- Token too big
- Reduce claims; use UserInfo endpoint instead; or move to reference tokens (with token introspection).
Security notes
- Don’t put secrets or internal IDs that should remain server‑side into tokens.
- Prefer opaque/reference tokens for critical APIs behind an introspection policy.
- Comply with privacy regulations (GDPR/DPDP): only expose necessary claims with explicit consent where applicable.
Design patterns & FAQs
Q: Roles vs Groups?
- Groups model membership (departments, tenants), often mapped to permissions via policy.
- Roles express permissions directly. Many teams use groups → roles mapping in authorization policies.
Q: OIDC vs SAML mappers?
- The idea is the same, but mapper types differ. Choose protocol accordingly in the scope.
Q: Multi‑tenant apps?
- Include a
tenantId
claim and validate it in every request. Consider realm per tenant only if isolation requirements demand it.
Q: Where does Keycloak store session/access data in the browser?
- The application typically stores the tokens (e.g., in memory/secure HTTP‑only cookies). Keycloak also sets cookies (e.g.,
KEYCLOAK_SESSION
,AUTH_SESSION_ID
) under your realm/host during flows, but apps are responsible for long‑term storage of tokens.
Q: Using Laravel Socialite—where are tokens saved?
- Socialite returns tokens to your Laravel app; you store them (usually hashed/rotated) in session or DB per your implementation. Socialite itself does not persist them long‑term.
Appendix — Sample combined token (excerpt)
{
"sub": "...",
"aud": "wizbrand-web",
"email": "rajesh@example.com",
"given_name": "Rajesh",
"family_name": "Kumar",
"groups": ["/wizbrand/hospital"],
"roles": ["admin","writer"],
"app": {
"department": "Finance",
"tenantId": "wiz-001",
"tier": "pro",
"is_admin": true,
"license": "enterprise"
},
"realm_access": {"roles": ["admin","user"]},
"resource_access": {"wizbrand-web": {"roles": ["writer","reader"]}}
}
Next steps
- Extract these steps into Terraform/Ansible for reproducible IAM.
- Add policy‑enforcement in your API gateway or microservices using these claims.
- Create a playground realm where teammates can practice mapper patterns safely.
Leave a Reply