IDENTITY BYTES
Senior IAM Architect Track / Phase 1: Core IAM Platform

LAB 02: Keycloak Realm Design
and OIDC Clients

Deploy an enterprise Identity Provider, federate it to the OpenLDAP directory you built in Lab 01, and issue your first standards based tokens. This is the lab where usernames and passwords become modern identity.

IB-SIA-02 Intermediate Est. 3 to 3.5 hours Keycloak / OIDC / LDAP Federation / JWT
Section 1

Lab Metadata

AttributeValue
Lab IDIB-SIA-02
TrackIdentity Bytes Senior IAM Architect Track (36 lab curriculum)
DifficultyIntermediate (requires IB-SIA-01; no prior Keycloak or OIDC knowledge assumed)
Core technologiesKeycloak 26.0 (quay.io/keycloak/keycloak:26.0), OpenLDAP from IB-SIA-01, Docker Engine 24+, curl, jq 1.6+
Protocols and standardsOpenID Connect Core 1.0, OAuth 2.0 (RFC 6749), JWT (RFC 7519), OIDC Discovery, LDAPv3 federation
Builds onIB-SIA-01 (the OpenLDAP directory becomes Keycloak's user store)
Feeds intoIB-SIA-03 (SAML federation), IB-SIA-04 (OIDC flows end to end), IB-SIA-05 (MFA), IB-SIA-06 (Keycloak HA)
Section 2

Lab Title and Description

Keycloak as the Enterprise Identity Provider: Realm Design, LDAP Federation, and Your First OIDC Client

Northgate Financial now has the central directory you built in Lab 01. Every employee exists once, groups answer authorisation questions, and TLS protects the wire. But a directory only answers one question at a time, over a protocol from 1993 that modern web and mobile applications do not speak. Each application still shows its own login page, still handles raw passwords, and still cannot offer single sign-on. The next architectural layer is an Identity Provider, IdP: a service that authenticates users once, on its own hardened login page, and then hands applications cryptographically signed tokens instead of passwords.

In this lab you deploy Keycloak, the leading open source Identity Provider and a CNCF project used across banking, government, and telecoms. You will design a realm for Northgate, federate it to your Lab 01 OpenLDAP directory so that Amina Smith and her colleagues can log in with the accounts they already have, import LDAP groups for authorisation, and register the HR portal as an OpenID Connect client. You will then obtain real tokens, decode them, and read the claims inside, the exact skill you use every time an SSO integration fails in production.

By the end, a password typed once at Keycloak's login page produces a signed JWT that any application can verify without ever seeing that password. That single sentence is the foundation of every SSO, federation, and Zero Trust architecture in the rest of this track. Estimated completion time is 3 to 3.5 hours.

Section 3

Prerequisites

3.1 Prior labs required

LabWhy it is required
IB-SIA-01Its OpenLDAP container becomes Keycloak's federated user store. You need the running containers, the ib-lab-net network, the directory structure, the four accounts, and their passwords.

Bring the Lab 01 environment back up before starting, and confirm it answers:

Restart and verify the Lab 01 environment
# Start the Lab 01 containers if they are stopped
docker start ib-openldap ib-phpldapadmin

# Confirm the directory still authenticates Amina Smith
ldapwhoami -x -H ldap://localhost:389 \
  -D "uid=asmith,ou=people,dc=identitybytes,dc=lab" -w "<USER_PASSWORD>"

Expected output: dn:uid=asmith,ou=people,dc=identitybytes,dc=lab. If you removed everything with Lab 01 Option B cleanup, rebuild Lab 01 Phases 1 to 4 first; it takes about twenty minutes on a second run.

3.2 System requirements

ResourceMinimumRecommended
Operating systemUbuntu 22.04, macOS 13, or Windows 11 with WSL2Ubuntu 22.04 LTS (all commands verified here)
RAM6 GB (Keycloak is a Java application and wants roughly 1 GB itself)8 GB
Disk12 GB free20 GB free
CPU2 cores4 cores
NetworkInternet access to pull images. Local port 8081 must be free (8080 is already used by phpLDAPadmin).

3.3 Required tools and versions

ToolVersionPurpose
Docker Engine24.0 or laterRuns the Keycloak container (installed in IB-SIA-01)
Keycloak imagequay.io/keycloak/keycloak:26.0 (pinned)The Identity Provider itself
curl7.8x or laterRequests tokens and reads OIDC endpoints from the command line
jq1.6 or laterFormats JSON responses and decodes JWT payloads

3.4 Installation commands

Ubuntu 22.04 / Debian / Windows WSL2 Ubuntu: install curl and jq
# curl sends HTTP requests; jq parses and pretty prints JSON.
# Together they are the identity engineer's token debugging toolkit.
sudo apt-get update && sudo apt-get install -y curl jq
macOS 13+: install jq (curl is built in)
brew install jq

3.5 Verification of tools

Verify curl and jq
curl --version | head -1
jq --version

Expected: a curl version line such as curl 7.81.0 ... and jq-1.6 or later. Any modern version passes.

VERIFICATIONLab 01 containers up, ldapwhoami succeeding, curl and jq responding: you are ready. Keep your Lab 01 admin password and user password to hand; both are used again here.
Section 4

Real World Problem Statement

This lab solves the problem that follows directory consolidation everywhere: password sprawl on the application side. Even with one directory, every LDAP integrated application still collects the user's password on its own login form and replays it to the directory. Every application is therefore a credential handling system, every login form is a phishing template, and capabilities such as MFA, session control, and single sign-on must be rebuilt application by application. The 2023 to 2025 wave of credential stuffing and session hijacking incidents across UK retail and finance all exploited exactly this per application authentication sprawl.

The enterprise answer is to centralise authentication itself, not only the account data. An Identity Provider owns the single login page, performs the password check (against the directory, via federation), enforces MFA and session policy in one place, and issues signed tokens that applications verify offline. Applications stop seeing passwords entirely. This pattern, IdP plus OIDC, is what Okta, Entra ID, Ping, and Keycloak all sell, and the role description this track targets names Keycloak explicitly as the reference platform.

Why it matters, across four dimensions

Risk

Every application that handles raw passwords is an attack surface. Token based access shrinks the credential exposure from dozens of login forms to one hardened IdP, and a stolen token expires in minutes while a stolen password lives for months.

Compliance

Centralised authentication gives one enforcement point for MFA and session policy, directly supporting PCI DSS 4.0 Requirement 8.4, ISO 27001 A.5.17, and NIST SP 800-63B assurance levels, with one audit log instead of ten.

Productivity

Users authenticate once per session for every connected application. Application teams delete their login code and their password reset tickets; a new integration becomes configuration, not development.

Security Posture

Signed, short lived, audience scoped tokens are the currency of Zero Trust. Everything later in this track, adaptive MFA, federation, workload identity, is delivered through the IdP you stand up today.

Concrete scenario

Northgate Financial, 5,000 employees, has completed directory consolidation. The security programme's next milestone reads: no application shall collect or transmit user passwords; all workforce authentication shall occur at a central Identity Provider with tokens issued under OpenID Connect. The HR portal is nominated as the first migration. Your task in this lab is the reference implementation: a Keycloak realm named northgate, federated to the corporate directory in read only mode, with LDAP groups imported for authorisation, and the HR portal registered as a confidential OIDC client receiving signed JWTs that carry the user's identity and group memberships as claims.

Section 5

Skills Mapped to Production Solutions

Skill LearnedReal World Enterprise Application
Deploying and bootstrapping KeycloakStanding up the workforce or customer IdP that Red Hat ships as RH-SSO / RHBK to banks, telecoms, and governments
Realm design and separationTenant isolation strategy: separating workforce, customer, and partner identity populations with independent policies, the Keycloak equivalent of Entra ID tenants or Okta orgs
LDAP user federation configurationConnecting any IdP to Active Directory or corporate LDAP, the single most common enterprise SSO deployment pattern, including attribute mapping and sync strategy decisions
Group mapping from directory to IdPCarrying AD or LDAP group based authorisation into cloud and SaaS applications through token claims
Registering confidential OIDC clientsOnboarding every new application to enterprise SSO: redirect URI hygiene, client authentication, and flow selection
Requesting tokens and decoding JWTs with curl and jqFrontline SSO troubleshooting: reading iss, aud, exp, and custom claims to diagnose why an application rejects a token
Claims engineering with protocol mappersShaping tokens to application contracts, such as adding group claims for RBAC, controlling PII exposure, and meeting data minimisation obligations
Using OIDC discovery metadataAutomating client configuration and federation setup against any standards compliant IdP, including during vendor integrations and M&A work
Section 6

Architecture Overview

YOUR WORKSTATION Sign in Browser: Keycloak login admin console + account console $ curl .../token "access_token": "eyJhbGciOiJSUzI1..." Terminal: curl + jq simulated HR portal client DOCKER NETWORK: ib-lab-net KEYCLOAK 26 ib-keycloak · host port 8081 REALM: northgate OIDC client hr-portal LDAP federation READ_ONLY Groups: finance-team · it-admins group-ldap-mapper → token claim SIGNED JWT (RS256) header: alg, kid payload: sub, aud, exp, groups signature OPENLDAP (LAB 01) ib-openldap · dc=identitybytes,dc=lab HTTPS 8081: login page token request issues LDAP 389: bind + search (container to container, internal)

Component breakdown

ComponentPurposeTechnologyDeploymentPortsKey configuration
KeycloakIdentity Provider: owns the login page, validates credentials against LDAP, manages sessions, issues OIDC tokensquay.io/keycloak/keycloak:26.0Docker container ib-keycloak on ib-lab-net, dev mode8081 on host, 8080 in containerBootstrap admin via KC_BOOTSTRAP_ADMIN_USERNAME and KC_BOOTSTRAP_ADMIN_PASSWORD; data persisted in volume ib-keycloak-data
Realm northgateIsolated identity population with its own users, clients, sessions, and policiesKeycloak realmCreated in the admin consolen/aEndpoints under /realms/northgate/; the built in master realm is reserved for platform administration
LDAP federation providerDelegates credential validation and user lookup to the Lab 01 directoryKeycloak user storage SPIConfigured inside the realm389 (container to container)Connection ldap://ib-openldap:389, bind as svc-hrportal, users DN ou=people,..., edit mode READ_ONLY
OIDC client hr-portalRepresents the HR application; the registered party allowed to request tokensOIDC confidential clientConfigured inside the realmn/aClient authentication on, standard flow on, redirect URI http://localhost:9090/*, group membership mapper adding a groups claim
OpenLDAP (Lab 01)Authoritative account store; source of truth for users and groupsosixia/openldap:1.5.0Existing container ib-openldap389, 636Unchanged from IB-SIA-01

Data flow

  1. Client redirects to the IdP: the HR portal sends the user's browser to Keycloak's authorisation endpoint in the northgate realm. Why: the application must never render its own password form; delegating the login page is the entire point of the pattern.
  2. Keycloak validates against LDAP: the user submits credentials to Keycloak, which searches ou=people for the username via the federation provider and then binds to OpenLDAP as that user's DN to verify the password. Why: read only federation means the directory remains the single source of truth and no password hash is copied into Keycloak.
  3. Session established, tokens issued: on success Keycloak creates an SSO session cookie and issues an ID token and access token, both signed with the realm's RS256 private key. Why: signatures let any application verify token authenticity offline using the realm's published public keys.
  4. Claims carry authorisation data: the group membership mapper embeds the user's LDAP groups into a groups claim. Why: the application makes RBAC decisions from the token alone, with no LDAP query of its own, decoupling applications from the directory.
  5. Client verifies and consumes: the HR portal (simulated by curl in this lab) validates issuer, audience, expiry, and signature via the discovery metadata and JWKS endpoint, then trusts the claims. Why: this verification checklist is precisely what you debug when an integration rejects tokens in production.

Security considerations

ControlIn this lab
Encryption in transitKeycloak runs HTTP in dev mode on localhost only; LDAP traffic is container to container on an isolated bridge network. Production requires TLS on both legs, addressed in Section 9
AuthenticationUser passwords are validated by LDAP bind through read only federation; Keycloak stores no password hashes for federated users; the platform admin uses a bootstrap account confined to the master realm
AuthorisationGroup memberships flow from LDAP into token claims through an explicit mapper; the client is confidential and must authenticate with its secret to redeem tokens
AuditKeycloak records login events per realm (enabled in this lab) and writes structured logs to stdout for docker logs; production ships both to a SIEM
Section 7

Step by Step Implementation

Phase 1: Deploy Keycloak

Step 1.1: Run the Keycloak container

Purpose

Launch Keycloak 26 in development mode on the Lab 01 network, with a bootstrap administrator and persistent storage.

Context

Keycloak is a Java application that, in production, runs behind TLS with an external database. Development mode (start-dev) relaxes those requirements so you can learn the identity concepts without a database build; Section 9 lists exactly what changes for production. Joining ib-lab-net matters because Keycloak will later reach the directory at the hostname ib-openldap. Choose a third password now for the Keycloak administrator, referred to below as <KC_ADMIN_PASSWORD>, distinct from your Lab 01 values.

Deploy the Keycloak container
# Deploy Keycloak 26 in development mode.
# Purpose: provides the Identity Provider for the whole track.
# Enterprise context: the open source engine behind Red Hat Build of Keycloak,
# used as the workforce and customer IdP in banking and government.
#
# SECURITY NOTE: start-dev disables TLS and strict hostname checking.
# Acceptable only because this listens on localhost in a lab.

docker run -d \
  --name ib-keycloak \
  --network ib-lab-net \
  -p 8081:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME="admin" \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD="<KC_ADMIN_PASSWORD>" \
  -v ib-keycloak-data:/opt/keycloak/data \
  quay.io/keycloak/keycloak:26.0 \
  start-dev

Line by line: port 8081 on your machine maps to Keycloak's 8080 inside the container, avoiding the clash with phpLDAPadmin. The two KC_BOOTSTRAP_ADMIN_* variables create the initial administrator on first start (Keycloak 26 renamed these from the older KEYCLOAK_ADMIN variables, so older tutorials will mislead you here). The volume persists Keycloak's embedded development database across restarts. start-dev is the command passed to the container, selecting development mode.

Expected outcome

Docker prints a container ID. First start takes 30 to 60 seconds while Keycloak builds and boots.

VERIFICATIONRun docker logs ib-keycloak 2>&1 | grep -i "started" after a minute. Expected: a line similar to Keycloak 26.0.x ... started in 15.2s. Listening on: http://0.0.0.0:8080. Then open http://localhost:8081 in your browser and confirm the Keycloak welcome page loads. If the log shows a port bind error, another process holds 8081; pick 8082 and adjust every URL that follows.
What just happened?You started a complete Identity Provider. On first boot it created its internal database in the volume, generated an RSA keypair for signing tokens in the master realm, and created your bootstrap administrator. Nothing knows about Northgate yet; that is the next phase.

Phase 2: Design the Northgate Realm

Step 2.1: Log in to the admin console and create the realm

Purpose

Create an isolated realm named northgate for all Northgate users, clients, and policies.

Context

A realm is Keycloak's isolation boundary: each realm has its own users, clients, sessions, signing keys, and login policies, comparable to a tenant in Entra ID or an org in Okta. The built in master realm exists to administer Keycloak itself, and using it for end users is a recognised anti pattern in the Keycloak server administration guide, because a compromise of any application in master is a compromise of the whole platform. Realm design, deciding how many realms and what lives in each, is a genuine architecture decision you will defend in interviews: one realm per identity population (workforce, customers, partners), not one per application.

Actions in the admin console
  1. Open http://localhost:8081 and click Administration Console. Log in as admin with <KC_ADMIN_PASSWORD>.
  2. In the top left, the realm selector currently shows Keycloak (the master realm). Click it, then click Create realm.
  3. Realm name: northgate (lower case, exactly, because it becomes part of every URL). Leave Enabled on. Click Create.
  4. Enable auditing while you are here: go to Realm settings, open the Sessions tab and note the default SSO session idle of 30 minutes, then open the User events settings area under Realm settings → Events (called Event configs in some 26.x builds), and switch Save events on for user events. Click Save.
Expected outcome

The realm selector now shows northgate, and everything you configure from here on happens inside it.

VERIFICATIONRun curl -s http://localhost:8081/realms/northgate/.well-known/openid-configuration | jq -r '.issuer'. Expected output: http://localhost:8081/realms/northgate. This discovery document is the realm's public, machine readable identity; a 404 means the realm name was typed differently, so check its exact spelling under Realm settings.
What just happened?Creating the realm generated a complete OIDC provider under a new URL path: its own RS256 signing keypair, its own token endpoints, and its own published metadata. The discovery document you fetched is how every standards compliant application on earth would learn to integrate with Northgate, and reading it with curl is a skill you will reuse against Okta, Entra ID, and every other IdP.

Phase 3: Federate the Lab 01 Directory

Step 3.1: Add the LDAP user federation provider

Purpose

Connect the realm to OpenLDAP so directory accounts can log in through Keycloak, in read only mode, with the directory remaining the source of truth.

Context

Federation here means delegation: Keycloak looks users up in LDAP and validates their passwords by binding to LDAP as them, exactly the four stage sequence you proved by hand in Lab 01 Section 8. We bind with the svc-hrportal service account rather than the directory administrator, applying least privilege: the connection only needs to read user entries, never write them. READ_ONLY edit mode enforces the same principle in the other direction, preventing anyone from modifying directory data through Keycloak.

Actions in the admin console (realm: northgate)
  1. Go to User federation in the left menu, click Add LDAP providers.
  2. General options: UI display name northgate-ldap. Vendor: Other.
  3. Connection and authentication settings: Connection URL ldap://ib-openldap:389 (the container name resolves because both containers share ib-lab-net). Click Test connection; a green success banner must appear. Bind type simple. Bind DN uid=svc-hrportal,ou=service-accounts,dc=identitybytes,dc=lab. Bind credentials: your <USER_PASSWORD> from Lab 01. Click Test authentication; a second success banner must appear.
  4. LDAP searching and updating: Edit mode READ_ONLY. Users DN ou=people,dc=identitybytes,dc=lab. Username LDAP attribute uid. RDN LDAP attribute uid. UUID LDAP attribute entryUUID. User object classes inetOrgPerson.
  5. Leave synchronisation settings at defaults for now and click Save.
  6. Back on the provider page, open the Action menu in the top right and choose Sync all users.
Expected outcome

The sync action reports 3 users imported (the service account sits outside ou=people, so it is correctly excluded from the human population).

VERIFICATIONGo to Users in the left menu and click Search with an empty query. Expected: asmith, jpatel, and lokafor listed with their email addresses from LDAP. If Test connection failed, the containers are not on the same network, which docker inspect ib-keycloak --format '{{json .NetworkSettings.Networks}}' | jq will confirm. If Test authentication failed, re-verify the service account password with the Lab 01 ldapwhoami command. If sync imported 0 users, the Users DN or object class value has a typo.
PRODUCTION CONSIDERATIONAgainst Active Directory, this same screen changes in three ways: Vendor becomes Active Directory, the username attribute becomes sAMAccountName, and the connection must be ldaps:// on 636, because AD rejects simple binds over cleartext when signing is enforced. Enterprises also schedule periodic sync and enable Kerberos integration on this provider, which is exactly the brokering you will build in IB-SIA-26.
What just happened?Keycloak connected to your directory as a least privilege service account, searched ou=people for entries of class inetOrgPerson, and created lightweight linked accounts for each. No passwords moved: when Amina logs in shortly, Keycloak will verify her password by binding to LDAP as her DN, live, every time.

Step 3.2: Prove federated login with the account console

Purpose

Perform the first real end user login through Keycloak, before any client exists, using Keycloak's built in account console as the test application.

Context

The account console is a small self service application that ships inside every realm, which makes it the perfect zero code test target: if a federated user can sign in there, the entire chain of browser, Keycloak, federation provider, and LDAP bind works.

Actions
  1. Open a private or incognito browser window (so your admin session does not interfere).
  2. Go to http://localhost:8081/realms/northgate/account.
  3. Sign in as asmith with the Lab 01 <USER_PASSWORD>.
Expected outcome

The account console loads and shows personal info populated from LDAP: first name Amina, last name Smith, email amina.smith@identitybytes.lab, with the fields read only because the federation is read only.

VERIFICATIONIn the admin console window, go to Sessions in the left menu. Expected: one session for user asmith. Then check the login event under Events → User events: a LOGIN event for asmith. A failed sign in with Invalid username or password means either the password differs from what Lab 01 set, which ldapwhoami settles in one command, or the federation username attribute is not uid.
What just happened?A password created in Lab 01 with ldappasswd, stored only in OpenLDAP, has authenticated a user through a modern IdP that never stored it. You watched a live delegation chain: browser to Keycloak over HTTP, Keycloak to LDAP over the container network, and the session now lives in Keycloak, which is what will make single sign-on possible across every future client.

Step 3.3: Import LDAP groups with a group mapper

Purpose

Map finance-team and it-admins from the directory into Keycloak groups, so authorisation data can flow into tokens.

Context

Federation providers use mappers to translate between directory schema and Keycloak's model. The group LDAP mapper reads groupOfNames entries and their DN valued member attributes, the exact structures you built in Lab 01 Step 4.4, and mirrors them as Keycloak groups with correct memberships.

Actions in the admin console (realm: northgate)
  1. Go to User federation → northgate-ldap, open the Mappers tab, click Add mapper.
  2. Name northgate-groups. Mapper type group-ldap-mapper.
  3. LDAP groups DN ou=groups,dc=identitybytes,dc=lab. Group name LDAP attribute cn. Group object classes groupOfNames. Membership LDAP attribute member. Membership attribute type DN. Mode READ_ONLY. Leave the remaining fields at defaults and click Save.
  4. From the mapper's Action menu, choose Sync LDAP groups to Keycloak.
Expected outcome

The sync reports 2 groups imported.

VERIFICATIONGo to Groups in the left menu: finance-team and it-admins are listed. Open Users → asmith → Groups: membership of finance-team shows. If groups imported but memberships are empty, the Membership attribute type is not set to DN, the classic mistake with groupOfNames.

Phase 4: Register the HR Portal as an OIDC Client

Step 4.1: Create the confidential client

Purpose

Register hr-portal as a confidential OIDC client: the named, authenticated party permitted to send users to Keycloak and redeem tokens.

Context

An OIDC client is an application's registration record at the IdP. Confidential means the application runs server side and can hold a secret, so Keycloak will refuse token requests that do not present it; the alternative, a public client, is for browser and mobile apps that cannot keep secrets and rely on PKCE instead, which IB-SIA-04 covers in depth. The redirect URI list is a security control, not a convenience: Keycloak will only ever send authorisation codes to exact matches, which is what prevents an attacker redirecting your users' codes to a hostile site.

Actions in the admin console (realm: northgate)
  1. Go to Clients, click Create client.
  2. Client type OpenID Connect. Client ID hr-portal. Name Northgate HR Portal. Click Next.
  3. Capability config: switch Client authentication ON (this is what makes it confidential). Under Authentication flow, keep Standard flow ticked, and additionally tick Direct access grants for this lab only, so we can demonstrate tokens from curl without building an application; the security analysis explains why this stays off in production. Click Next.
  4. Login settings: Root URL http://localhost:9090. Valid redirect URIs http://localhost:9090/*. Web origins http://localhost:9090. Click Save.
  5. Open the client's Credentials tab and copy the Client secret. This value is <CLIENT_SECRET> in every command below.
Expected outcome

The client appears in the list with a Credentials tab present, confirming it is confidential.

SECURITY WARNINGThe client secret is a credential of the same sensitivity as a service account password. It never goes into Git, chat, or a screenshot. When you publish this lab in Section 12, regenerate the secret afterwards from the same Credentials tab, a one click rotation that is itself good production practice.

Step 4.2: Add a groups claim with a protocol mapper

Purpose

Shape the token so it carries the user's group memberships, turning the token into a complete authentication and authorisation statement.

Context

By default, Keycloak keeps tokens minimal and does not include groups. Protocol mappers are the claims engineering layer: per client rules that add, rename, or transform claims. Deciding what goes into a token is a real architectural judgement, balancing application needs against token size and data minimisation under UK GDPR, and this mapper is your first deliberate decision of that kind.

Actions in the admin console (realm: northgate)
  1. Go to Clients → hr-portal, open the Client scopes tab, and click the scope named hr-portal-dedicated.
  2. Click Configure a new mapper (or Add mapper, By configuration), and choose Group Membership.
  3. Name groups. Token claim name groups. Switch Full group path OFF, so the claim reads finance-team rather than /finance-team. Keep Add to ID token, Add to access token, and Add to userinfo ON. Click Save.
Expected outcome

The dedicated scope lists a mapper named groups. Proof arrives in the next step, inside a real token.

Phase 5: Issue and Read Real Tokens

Step 5.1: Request tokens from the command line

Purpose

Act as the HR portal: authenticate as the client, present Amina's credentials through the direct access grant, and receive her tokens.

Context

The direct access grant (the OAuth resource owner password flow) lets a client exchange a username and password for tokens in one call. It is deprecated for real applications, because it puts the password back in the client's hands, defeating the pattern; we enable it in the lab purely because it makes token mechanics visible in a single curl command. IB-SIA-04 replaces it with the browser based authorisation code flow plus PKCE, the production standard.

Request tokens for asmith via the token endpoint
# Ask the northgate token endpoint for tokens.
# The client authenticates with its ID and secret; the user authenticates
# with username and password; scope openid requests an ID token too.

curl -s -X POST http://localhost:8081/realms/northgate/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=hr-portal \
  -d client_secret="<CLIENT_SECRET>" \
  -d username=asmith \
  -d password="<USER_PASSWORD>" \
  -d scope=openid | jq . | tee /tmp/tokens.json
Expected outcome
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
  "scope": "openid profile email"
}
VERIFICATIONThe response contains access_token, id_token, and "expires_in": 300, meaning a five minute access token lifetime, Keycloak's deliberate default: short enough that a stolen token has a small window, long enough to avoid constant refreshes. An unauthorized_client error means Direct access grants is not ticked on the client; invalid_client means the secret is wrong; invalid_grant means Amina's credentials failed, which sends you back to the Lab 01 ldapwhoami check.

Step 5.2: Decode the JWT and read its claims

Purpose

Open the access token and read the identity and authorisation statements inside, the core diagnostic skill of SSO operations.

Context

A JWT is three base64url encoded parts joined by dots: a header naming the signature algorithm and key ID, a payload of claims, and the signature. Decoding is not decrypting; anyone holding a JWT can read it, which is why tokens must never carry secrets and why possession of a token is treated as sensitive. Trust comes from the signature, which only the realm's private key can produce.

Decode the access token payload with jq
# Extract the access token, take its middle segment, base64url decode it,
# and parse as JSON. jq's @base64d handles the decoding.

jq -r '.access_token | split(".")[1] | @base64d | fromjson' /tmp/tokens.json
Expected outcome
{
  "exp": 1751791234,
  "iat": 1751790934,
  "iss": "http://localhost:8081/realms/northgate",
  "aud": "account",
  "sub": "f:2f9a...:asmith",
  "typ": "Bearer",
  "azp": "hr-portal",
  "preferred_username": "asmith",
  "email": "amina.smith@identitybytes.lab",
  "name": "Amina Smith",
  "groups": [
    "finance-team"
  ],
  ...
}
VERIFICATIONFour checks: iss equals your realm URL, azp (authorised party) equals hr-portal, preferred_username is asmith, and groups contains finance-team, proving the entire chain from a Lab 01 LDIF file to a claim inside a signed token. If groups is absent, the Step 4.2 mapper is either missing or not saved on the dedicated client scope.
INFOClaim vocabulary worth memorising: iss who issued it, sub the stable subject identifier, aud who may consume it, exp and iat the validity window, azp which client requested it. Every token rejection you will ever debug is one of these five failing a check.
What just happened?You read a token exactly as a resource server does before the signature check. The user's identity, from LDAP; her group, from a Lab 01 LDIF file, through a federation mapper, through a protocol mapper; and the trust metadata that binds it all to one issuer and one client, all inside a portable, verifiable document that expires in five minutes.

Step 5.3: Verify the signature chain via JWKS

Purpose

Confirm the token's signing key ID matches a public key the realm publishes, which is how applications verify tokens without contacting Keycloak per request.

Match the token kid to the realm JWKS
# The token header names the key that signed it
jq -r '.access_token | split(".")[0] | @base64d | fromjson | .kid' /tmp/tokens.json

# The realm publishes its public keys at the JWKS endpoint from discovery
curl -s http://localhost:8081/realms/northgate/protocol/openid-connect/certs \
  | jq -r '.keys[] | select(.alg=="RS256") | .kid'
Expected outcome

The two commands print the same key ID string.

VERIFICATIONMatching kid values pass. This is the offline verification model: applications cache the JWKS, match kid, and verify signatures locally, which is why an IdP can serve millions of token verifications without receiving a single one of them.
Section 8

Testing and Validation

End to end scenario: one identity, two doors, one source of truth

This test proves the architecture rather than a single feature: the same LDAP account authenticates through the browser and through the token endpoint, group data flows to both, and the directory remains the sole password store.

Run the end to end validation
# STAGE 1: browser SSO. In a private window, sign in to
# http://localhost:8081/realms/northgate/account as jpatel.
# Expected: account console loads showing Jay Patel from LDAP.

# STAGE 2: token issuance for the same user via curl
curl -s -X POST http://localhost:8081/realms/northgate/protocol/openid-connect/token \
  -d grant_type=password -d client_id=hr-portal \
  -d client_secret="<CLIENT_SECRET>" \
  -d username=jpatel -d password="<USER_PASSWORD>" -d scope=openid \
  | jq -r '.access_token | split(".")[1] | @base64d | fromjson | {user: .preferred_username, groups}'
# Expected: {"user": "jpatel", "groups": ["it-admins"]}

# STAGE 3: prove Keycloak holds no password for federated users.
# In the admin console: Users, jpatel, Credentials tab.
# Expected: no password credential is stored; the tab shows none.

# STAGE 4: prove the directory is still the enforcement point.
# Change Jay's password in LDAP, then confirm the OLD password now fails
# and the NEW one succeeds at the token endpoint, with no Keycloak change.
ldappasswd -x -H ldap://localhost:389 \
  -D "cn=admin,dc=identitybytes,dc=lab" -w "<YOUR_ADMIN_PASSWORD>" \
  -s "<NEW_USER_PASSWORD>" "uid=jpatel,ou=people,dc=identitybytes,dc=lab"

Negative tests

Run the negative tests
# TEST N1: wrong user password must yield invalid_grant, never a token
curl -s -X POST http://localhost:8081/realms/northgate/protocol/openid-connect/token \
  -d grant_type=password -d client_id=hr-portal \
  -d client_secret="<CLIENT_SECRET>" \
  -d username=asmith -d password="WrongPassword1" | jq .
# Expected: {"error":"invalid_grant","error_description":"Invalid user credentials"}

# TEST N2: wrong client secret must be rejected before the user is even checked
curl -s -X POST http://localhost:8081/realms/northgate/protocol/openid-connect/token \
  -d grant_type=password -d client_id=hr-portal \
  -d client_secret="not-the-secret" \
  -d username=asmith -d password="<USER_PASSWORD>" | jq .
# Expected: {"error":"invalid_client", ...}
# Two independent authentications guard the token endpoint: the client's
# and the user's. Both must pass.

# TEST N3: the master realm must not know northgate users at all
curl -s -X POST http://localhost:8081/realms/master/protocol/openid-connect/token \
  -d grant_type=password -d client_id=hr-portal \
  -d client_secret="<CLIENT_SECRET>" \
  -d username=asmith -d password="<USER_PASSWORD>" | jq .
# Expected: {"error":"invalid_client", ...} because hr-portal does not
# exist in master. Realm isolation working as designed.

# TEST N4: expired tokens must fail introspection after 5 minutes
# Wait for expiry (or come back after a break), then:
ACCESS=$(jq -r '.access_token' /tmp/tokens.json)
curl -s -X POST http://localhost:8081/realms/northgate/protocol/openid-connect/token/introspect \
  -d client_id=hr-portal -d client_secret="<CLIENT_SECRET>" \
  -d token="$ACCESS" | jq '.active'
# Expected: false

Common failure modes and solutions

SymptomLikely causeSolution
Test connection fails in the federation screenKeycloak and OpenLDAP are on different Docker networks, or the connection URL uses localhostThe URL must be ldap://ib-openldap:389; from inside the Keycloak container, localhost is Keycloak itself. Confirm both containers list ib-lab-net in docker inspect
Test authentication failsWrong bind DN or service account passwordValidate the exact DN and password with ldapwhoami from the host first, then paste identical values
Sync imports 0 usersUsers DN typo, or object class mismatchCompare against a raw ldapsearch of ou=people; the values must match what the directory actually returns
Login page rejects valid LDAP usersUsername LDAP attribute not set to uidCorrect the attribute on the federation provider, then Sync all users again
unauthorized_client at the token endpointDirect access grants not enabled on hr-portalClients, hr-portal, Settings, tick Direct access grants, Save
groups claim missing from tokensMapper created outside the client's dedicated scope, or not savedRecreate the Group Membership mapper under Client scopes, hr-portal-dedicated
Admin console unreachable after rebootContainers stoppeddocker start ib-openldap ib-keycloak; Keycloak state persists in its volume
Section 9

Security Analysis

What makes this implementation secure

What is intentionally simplified for the lab

Production hardening recommendations

RecommendationWhyCovered in
Run start --optimized with TLS, a strict hostname, and external PostgreSQLDev mode is explicitly unsupported for production by the Keycloak team; H2 loses data and cannot clusterIB-SIA-06 (Keycloak HA with external Postgres)
Switch federation to ldaps:// with a CA issued certificate in Keycloak's truststoreThe bind leg carries user passwords at every login; it deserves the same protection as the front doorIB-SIA-17 to 19 (PKI phase) applied back to this provider
Disable direct access grants on every client; use authorisation code with PKCEThe password grant returns credential handling to applications and bypasses MFA and adaptive controlsIB-SIA-04
Enforce MFA at the realm level and step up for sensitive clientsCentralised authentication makes the IdP the single most valuable target; NIST SP 800-63B AAL2 requires a second factorIB-SIA-05 and IB-SIA-08
Protect and monitor the master realm: rename or remove the bootstrap admin, require MFA, alert on master realm loginsMaster realm compromise is platform compromiseReinforced in IB-SIA-33 (identity incident response)
Ship user and admin events to a SIEM with alerts on failed login spikes and client secret changesPassword spraying against an IdP is visible early only if someone is watching the events you enabled todayIB-SIA-35 (identity observability)
Rotate the hr-portal client secret on a schedule, or replace it with private_key_jwt client authenticationStatic shared secrets age badly; asymmetric client auth removes the shared secret entirelyIB-SIA-10 (OAuth 2.1 hardening)
Section 10

Cleanup Instructions

Option A: pause the lab, keep everything (recommended, later labs reuse this build)

Stop containers without losing data
# Stops all track containers. Realm, federation, and client survive in volumes.
docker stop ib-keycloak ib-openldap ib-phpldapadmin

# Resume later with:
docker start ib-openldap ib-keycloak ib-phpldapadmin

Option B: remove Lab 02 only, keep Lab 01

Remove Keycloak completely, preserve the directory
# Remove the Keycloak container and its data volume.
# WARNING: deletes the realm, federation config, and client permanently.
docker rm -f ib-keycloak
docker volume rm ib-keycloak-data

# Remove the token capture file
rm -f /tmp/tokens.json

Option C: full track teardown

Remove everything from Labs 01 and 02
docker rm -f ib-keycloak ib-openldap ib-phpldapadmin
docker volume rm ib-keycloak-data ib-ldap-data ib-ldap-config
docker network rm ib-lab-net
docker rmi quay.io/keycloak/keycloak:26.0 osixia/openldap:1.5.0 osixia/phpldapadmin:0.9.0
rm -f /tmp/tokens.json
VERIFICATIONAfter Option B: docker ps -a | grep ib-keycloak and docker volume ls | grep ib-keycloak both return nothing, while ldapwhoami against Lab 01 still succeeds. After Option C: all ib- prefixed containers, volumes, and the network are gone. Keep your notes and any exported realm JSON; they are portfolio evidence.
Section 11

Recommended Learning Links

Section 12 (Bonus)

Portfolio Publishing Guide: Growing the Repository and the Narrative

Lab 01 created the repository; Lab 02 starts the compounding. The publishing pattern from here on is a rhythm: sanitise, add the lab folder, export configuration as evidence, commit with a message that tells a story, push, post. Twenty minutes per lab, forever.

12.1 Export the realm as portfolio evidence

Configuration you can show beats configuration you describe. Keycloak exports a realm as JSON, which becomes the reviewable artefact of this lab, the equivalent of Lab 01's LDIF files.

Export the northgate realm to JSON
# Run the exporter inside the container, writing to the data volume,
# then copy the file out to your machine.
docker exec ib-keycloak /opt/keycloak/bin/kc.sh export \
  --dir /opt/keycloak/data/export --realm northgate --users skip

docker cp ib-keycloak:/opt/keycloak/data/export/northgate-realm.json ./northgate-realm.json

# SANITISE before it goes anywhere near Git: remove the client secret.
# This one jq line blanks every secret field in the export.
jq '(.clients[]? | select(has("secret")) | .secret) = "REDACTED"' \
  northgate-realm.json > northgate-realm.sanitised.json

# Confirm no secret survived. Expected output: nothing.
grep -o '"secret": "[^R][^"]*"' northgate-realm.sanitised.json

We export with --users skip because federated user data belongs to the directory, not the realm definition, and because portfolio repositories should carry structure, never people.

SECURITY WARNINGOnly the sanitised file enters the repository. Delete the raw export with rm northgate-realm.json once the sanitised copy is verified, and rotate the hr-portal client secret in the Credentials tab after publishing, so even an accidental leak has nothing to unlock.

12.2 Add Lab 02 to the repository and push

Create the lab folder, README, commit, and push
cd ~/identity-bytes-architect-labs
mkdir -p lab-02-keycloak-oidc/{docs,config,screenshots}

# Move in the artefacts: this guide and the sanitised realm export
cp <PATH_TO>/IB-SIA-02-keycloak-realm-oidc.html lab-02-keycloak-oidc/docs/
cp <PATH_TO>/northgate-realm.sanitised.json lab-02-keycloak-oidc/config/

cat > lab-02-keycloak-oidc/README.md << 'EOF'
# Lab 02: Keycloak Realm Design and OIDC Clients

Part of my Identity Bytes Senior IAM Architect lab series (IB-SIA-02).
Builds directly on Lab 01: the OpenLDAP directory becomes the federated
user store behind a modern Identity Provider.

## Problem this solves
Password sprawl on the application side. Even with one directory, every
application still collects passwords on its own login form. This lab
centralises authentication itself: one hardened login page, signed OIDC
tokens for applications, zero password handling outside the IdP.

## What I built
- Keycloak 26 (Docker, version pinned) with persistent storage
- Realm design: northgate workforce realm, master reserved for platform admin
- Read only LDAP federation binding as a least privilege service account
- LDAP group import via group-ldap-mapper (finance-team, it-admins)
- Confidential OIDC client (hr-portal) with a Group Membership protocol
  mapper adding a groups claim to tokens
- Token issuance, JWT decoding, JWKS signature chain verification, and
  token introspection, all from the command line

## Skills demonstrated
IdP deployment and realm architecture, LDAP/AD federation patterns,
claims engineering, OIDC discovery, JWT anatomy (iss/sub/aud/exp/azp),
negative security testing, and dev-versus-production hardening analysis.

## Key verification
A password stored only in LDAP authenticated a user through Keycloak;
her Lab 01 group arrived inside a signed RS256 JWT; wrong passwords,
wrong client secrets, and cross realm requests all failed correctly;
and Keycloak's credential store for federated users was provably empty.

Full step by step guide: docs/IB-SIA-02-keycloak-realm-oidc.html
Sanitised realm export: config/northgate-realm.sanitised.json
EOF

# Add screenshots: the northgate realm dashboard, the federation provider
# with green test banners, and the decoded token showing the groups claim.

git add .
git commit -m "Lab 02: Keycloak IdP with read only LDAP federation, northgate realm, hr-portal OIDC client, groups claim"
git push
VERIFICATIONThe GitHub repository now shows two lab folders, and the track level README's lab index gains a second row (add it: one line, one link). A recruiter clicking through sees progression, which is the entire point of a series over scattered one offs.

12.3 Update the track README index

Keep the shop window current
# Append the lab index entry to the repository root README.md, for example:
#
# | Lab | Focus | Status |
# |-----|-------|--------|
# | 01  | OpenLDAP enterprise directory, LDIF, LDAPS | Complete |
# | 02  | Keycloak IdP, LDAP federation, OIDC client, JWT claims | Complete |
# | 03  | SAML 2.0 federation | In progress |
git add README.md && git commit -m "Index: add Lab 02" && git push

12.4 Share it on LinkedIn

Attach one image, the decoded token with the groups claim visible (secrets redacted), because a claim that travelled from an LDIF file into a signed JWT is the most shareable artefact this lab produces. GitHub link in the first comment, not the body. A draft in the Identity Bytes style:

Draft post:

Last week I built a directory. This week I made it invisible.

That sounds like a step backwards, so here is the problem. Even after an organisation consolidates every account into one directory, each application still shows its own login form and handles the user's raw password. Ten applications means ten credential handling systems, ten phishing templates, and ten places MFA has to be bolted on separately.

The fix is an Identity Provider. This weekend I deployed Keycloak in front of my Lab 01 OpenLDAP build, federated in read only mode through a least privilege service account, so the directory stays the single source of truth and the IdP never stores a password hash. Then I registered a confidential OpenID Connect client and watched something quietly satisfying: a group I created two weeks ago in a plain text LDIF file arrived inside a signed, five minute JWT as an authorisation claim, verified against the realm's published keys.

The insight that stuck: federation is delegation with a contract. Keycloak does not copy identity, it borrows it, live, on every login, and the negative tests prove the boundaries hold. Wrong password, refused. Wrong client secret, refused before the user is even checked. Right credentials in the wrong realm, refused.

The stakes are simple. Every credential stuffing headline of the past three years exploited applications that handled passwords they never needed to see. Token based architectures exist so that stolen passwords meet MFA at one door, and stolen tokens die in minutes.

This is lab two of thirty six in the senior IAM architecture series I am publishing. Guide, sanitised realm export, and verification evidence are on my GitHub, link in the comments.

Honest question for fellow practitioners: how many applications in your estate still collect passwords on their own login forms?

Same mechanics as Lab 01: publish Tuesday to Thursday, 8 to 10 in the morning UK time, reply to every comment inside two hours, hashtags at the end (#IAM #Keycloak #OIDC #IdentityManagement #CyberSecurity), and reference the Lab 01 post in a comment so new readers find the series start. Consistency of cadence matters more than perfection of any single post.

12.5 Interview leverage from this lab

This lab evidences four lines of the Senior IAM Platform Architect role description directly: operating Keycloak as an identity provider, leading identity integrations against a directory, defining token strategies, and OIDC expertise. When asked to whiteboard an SSO rollout, you now describe a system you have built, broken, and verified, and the difference is audible to any interviewer who has done the work themselves.