DEV Community

iapilgrim
iapilgrim

Posted on

Keycloak from Scratch, Part 1 — Realms, Roles, Clients & Users (No Docker Magic, Just Clicks)

If you've ever inherited a Keycloak instance and had no idea what a
"realm" actually isolates, or why some clients have secrets and others
don't, this post is the ground floor. We're building one small realm by
hand, in the Admin Console — no automation scripts, no realm-export JSON
to skim past. Just enough clicking to make every concept stick.

By the end you'll have:

  • A dedicated realm
  • Three roles representing a basic permission tier (admin, editor, viewer)
  • A confidential client configured for direct login
  • Three users, each mapped to one role
  • A working curl command that logs in as each user and shows you their role inside a decoded JWT

Prerequisites

  • Docker + Docker Compose
  • curl and python3 (for a one-line JWT decode — no library needed)

1. Start Keycloak

# docker-compose.yml
services:
  keycloak:
    image: quay.io/keycloak/keycloak:26.7.0
    container_name: keycloak-fundamentals
    command: start-dev
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports:
      - "8080:8080"
Enter fullscreen mode Exit fullscreen mode
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Give it about 30 seconds, then open http://localhost:8080 and log in
with admin / admin.

start-dev is a dev-mode flag — it skips TLS and some production
checks so you can iterate quickly. Never run this flag in production.

2. Create a realm

A realm is Keycloak's top-level tenancy boundary — its own users,
roles, clients, and even its own login theme, fully isolated from every
other realm on the same server. Think of it as a separate mini
directory service you can spin up per environment, per product, or per
customer.

  • Top-left realm dropdown → Create realm
  • Realm name: fundamentals
  • Create

3. Create three realm roles

Roles are how you express "what can this identity do" without hardcoding
usernames into your authorization logic. We're using realm roles
here — visible to every client in the realm, as opposed to client
roles
, which are scoped to one specific client. (We'll hit client
roles in a later post when we build a proper RBAC API.)

  • Left sidebar → Realm rolesCreate role
  • Create three, one at a time: admin, editor, viewer

4. Create a confidential client

  • Left sidebar → ClientsCreate client
  • Client ID: kc-fundamentals-appNext
  • Client authentication: toggle On

This is the setting that makes a client "confidential" — it gets a
secret, and Keycloak requires that secret before it'll hand out a
token. A public client (toggle off) has no secret at all, because
anything shipped to a browser or mobile app can't actually keep one
secret. We'll build a public client in Part 2.

  • Capability config: check Direct access grants, leave Standard flow on if you like (harmless either way for this demo) → Save
  • Go to the Credentials tab and copy the Client secret — you'll paste it into the curl command later

5. Create three users

Repeat this for alice, bob, and carol:

  • UsersAdd user
  • Username: alice
  • First name: Alice, Last name: Demo

Don't skip this. Keycloak has a built-in "Update Profile" required
action that checks profile completeness dynamically, at login
time
— not something you'll see if you inspect the user's stored
data beforehand. An empty first/last name silently blocks login later
with a generic "Account is not fully set up" error that has nothing
to do with the password you set. This one cost me an embarrassing
amount of debugging the first time — save yourself the trouble.

  • Create
  • Credentials tab → Set passwordalice → toggle Temporary: OffSave

"Temporary" means the user is forced to change it on next login —
fine for humans clicking through a browser flow, but it'll break a
scripted or API-driven login every time, since there's no browser to
serve the "set a new password" screen. Off, for this demo.

  • Role mapping tab → Assign role → pick admin

Repeat for bobeditor and carolviewer.

6. One more thing to check: OTP

Some Keycloak versions ship the built-in direct grant authentication
flow with its conditional-OTP branch misconfigured as Required instead
of Conditional. If that's the case on your instance, every login
attempt from a user without an OTP device configured — which is all
three of ours — gets rejected before the password is even checked.

  • Authentication (left sidebar) → Flows tab → select direct grant
  • Find the OTP-related row at the top level (often labeled something like "Direct Grant - Conditional OTP")
  • If it's not already Conditional or Disabled, set it to Disabled

7. Log in and inspect the token

This is a Direct Access Grant (also called Resource Owner Password
Credentials, or ROPC) — the client trades a username and password
directly for a token, no browser redirect involved. It's convenient for
this fundamentals demo and for machine-to-machine scripts, but it means
the client sees the raw password, so it's a poor fit for anything
user-facing. (Part 2 covers the flow you actually want for a browser app:
Authorization Code + PKCE.)

curl -s -X POST http://localhost:8080/realms/fundamentals/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "client_id=kc-fundamentals-app" \
  -d "client_secret=PASTE_YOUR_SECRET_HERE" \
  -d "username=alice" \
  -d "password=alice" | python3 -m json.tool
Enter fullscreen mode Exit fullscreen mode

If that returns an access_token, decode its payload to see the role
claim in the flesh:

curl -s -X POST http://localhost:8080/realms/fundamentals/protocol/openid-connect/token \
  -d "grant_type=password" -d "client_id=kc-fundamentals-app" \
  -d "client_secret=PASTE_YOUR_SECRET_HERE" -d "username=alice" -d "password=alice" \
  | python3 -c "
import sys, json, base64
token = json.load(sys.stdin)['access_token']
payload = token.split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
"
Enter fullscreen mode Exit fullscreen mode

Look for:

"realm_access": {
  "roles": ["admin", "default-roles-fundamentals", "offline_access", "uma_authorization"]
}
Enter fullscreen mode Exit fullscreen mode

admin is the one we assigned. The other three are Keycloak's own
housekeeping roles every user gets by default.

Repeat with bob/bob and carol/carol — you should see editor
and viewer respectively.

What actually happened here

  • Realm isolation: fundamentals never sees or touches any other realm's users, clients, or roles — that's the whole point of realms.
  • Confidential client + Direct Grant: fine for this demo and for trusted server-side/CLI tooling, wrong for a browser SPA (that's Part 2).
  • Role claims propagate automatically: once a realm role is assigned to a user, Keycloak embeds it in every token that user gets, with zero extra configuration — this is what your API or gateway will check against later.

Troubleshooting: "Account is not fully set up"

If you hit this, it's one of exactly two causes:

  1. OTP required in the direct grant flow (see step 6 above)
  2. Incomplete user profile — missing first/last name (see step 5). This one is genuinely invisible from the outside: it's evaluated live at login, not stored anywhere you can query beforehand.

Check both. They're independent, and fixing only one can still leave you
stuck.


Next up, Part 2: we swap Direct Grant for the flow every real
browser app should use — Authorization Code with PKCE — and build a
vanilla-JS SPA that implements the whole thing by hand, no auth library,
so every step of the exchange is visible.

Top comments (0)