Keycloak Client Scopes & Mappers — Step‑by‑Step, Hands‑On Tutorial

Posted by

Limited Time Offer!

For Less Than the Cost of a Starbucks Coffee, Access All DevOpsSchool Videos on YouTube Unlimitedly.
Master DevOps, SRE, DevSecOps Skills!

Enroll Now

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:

  1. User Property — maps built‑in user fields (username, email, first/last name).
  2. User Attribute — maps custom attributes you add to users (e.g., department, tenantId).
  3. Group Membership — adds user’s groups as a claim (optionally with full path).
  4. Role mappers — project roles into custom claims (beyond the default realm_access/resource_access).
  5. Script Mapper — generate claims with custom logic.
  6. 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.

  1. Create/verify realm: Realm name: wizbrand.
  2. Create clientwizbrand-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
  3. Create user rajesh with a password, verify Email Verified if you plan to expose email 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.

  1. Create Client Scope
    • Name: app-common
    • Protocol: OpenID Connect
  2. 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: ✅
    • User Property → given_name(first name)
      • Token Claim Name: given_name
      • ID token ✅, UserInfo ✅
    • User Property → family_name(last name)
      • Token Claim Name: family_name
      • ID token ✅, UserInfo ✅

You could alternatively attach Keycloak’s built‑in profile/email scopes, but creating your own makes intent explicit and avoids surprises.

  1. Attach the scope to the client
    • Client → Client scopesAdd as Default → select app-common.
  2. 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.

  1. Create groups
    • hospital
    • doctor
    • patient
      Optionally nest them: /wizbrand/hospital, /wizbrand/doctor, /wizbrand/patient.
  2. Assign user rajesh to one or more groups.
  3. 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 just hospital)
    • ID token: ✅ Access token: ✅ UserInfo: ✅
  4. 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.

  1. 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)
  2. 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.

  1. Add attributes to user rajesh:
    • department = Finance
    • tenantId = wiz-001
    • tier = pro
  2. 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 ✅
    Repeat for tenantIdapp.tenantId, and tierapp.tier.
  3. 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.* or https://wizbrand.com/claims/*) avoids collisions with standard OIDC fields.


Lab 5 — Scripted claim (dynamic logic)

Goal: Compute a claim based on conditions.

  1. Add a Script Mapper to app-common
    • Token Claim Name: app.is_admin
    • Script (example):
// 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 ✅
  1. 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.

  1. Add Hardcoded Claim to app-common
    • Token Claim Name: app.license
    • Claim value: enterprise
    • Claim JSON Type: String
    • ID token ✅, Access token ✅

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 wherever kcadm.sh is available.

  1. Login
kcadm.sh config credentials --server http://localhost:8080/auth \
  --realm master --user admin --password 'admin'
  1. Create client scope
kcadm.sh create client-scopes -r wizbrand -s name=app-common -s protocol=openid-connect
  1. 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
  1. 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.* or https://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 vs String)?
    • 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

Your email address will not be published. Required fields are marked *

0
Would love your thoughts, please comment.x
()
x