Hands-On: Your First IAM Implementation
Reading about IAM protocols is necessary. Building with them is what makes the knowledge stick. This chapter walks you through practical implementations that you can complete in a weekend, add to your portfolio, and discuss confidently in interviews.
These are not toy examples. Each project mirrors real-world IAM tasks that engineers perform daily. The difference is scope - we are building for learning, not for production. But the patterns, protocols, and debugging skills are identical.
Project 1: SSO Implementation with Keycloak
Keycloak is an open-source identity provider maintained by Red Hat. It supports SAML 2.0, OpenID Connect, OAuth 2.0, and user federation. Setting it up is the single best learning exercise for understanding how enterprise SSO works.
Setting Up Keycloak
Prerequisites: Docker installed on your machine.
# Pull and run Keycloak in development mode
docker run -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-dev
Navigate to http://localhost:8080 and log in with admin/admin.
Creating a Realm and Users
A realm in Keycloak is equivalent to a tenant. Think of it as an isolated identity domain.
- Create a new realm called "iam-lab"
- Create two users: alice (Engineering department) and bob (Finance department)
- Set passwords for both users
- Create two groups: "engineering" and "finance"
- Assign alice to engineering and bob to finance
Configuring an OIDC Client
This simulates connecting a web application to your IdP for SSO.
- In the iam-lab realm, create a new client
- Client ID: "demo-app"
- Client protocol: openid-connect
- Access type: confidential
- Valid redirect URIs: http://localhost:3000/callback
- Note the client secret from the Credentials tab
Building a Simple Application
Here is a minimal Node.js application that authenticates users via Keycloak:
// app.js - Minimal OIDC relying party
const express = require('express');
const session = require('express-session');
const { Issuer, generators } = require('openid-client');
const app = express();
app.use(session({
secret: 'iam-lab-secret',
resave: false,
saveUninitialized: false
}));
async function setup() {
// Discover the OIDC provider configuration
const keycloakIssuer = await Issuer.discover(
'http://localhost:8080/realms/iam-lab'
);
const client = new keycloakIssuer.Client({
client_id: 'demo-app',
client_secret: 'YOUR_CLIENT_SECRET_HERE',
redirect_uris: ['http://localhost:3000/callback'],
response_types: ['code']
});
// Login route - redirects to Keycloak
app.get('/login', (req, res) => {
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
req.session.codeVerifier = codeVerifier;
const authUrl = client.authorizationUrl({
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
res.redirect(authUrl);
});
// Callback route - exchanges code for tokens
app.get('/callback', async (req, res) => {
const params = client.callbackParams(req);
const tokenSet = await client.callback(
'http://localhost:3000/callback',
params,
{ code_verifier: req.session.codeVerifier }
);
req.session.userInfo = await client.userinfo(tokenSet);
req.session.idToken = tokenSet.id_token;
res.redirect('/profile');
});
// Protected route - shows user info
app.get('/profile', (req, res) => {
if (!req.session.userInfo) {
return res.redirect('/login');
}
res.json({
message: 'Authenticated successfully',
user: req.session.userInfo
});
});
app.listen(3000, () => {
console.log('Demo app running on http://localhost:3000');
});
}
setup();
WHAT YOU JUST BUILT
===================
Browser Your App (RP) Keycloak (IdP)
| | |
| GET /login | |
|----------------->| |
| | |
| 302 Redirect | |
|<-----------------| |
| |
| GET /realms/iam-lab/protocol/openid-connect/auth
|----------------------------------------->|
| |
| Keycloak login page |
|<-----------------------------------------|
| |
| POST (username + password) |
|----------------------------------------->|
| |
| 302 Redirect with ?code=abc123 |
|<-----------------------------------------|
| | |
| GET /callback | |
| ?code=abc123 | |
|----------------->| |
| | |
| | POST /token |
| | (code + PKCE verifier) |
| |---------------------->|
| | |
| | {access_token, id_token}
| |<----------------------|
| | |
| User profile | |
|<-----------------| |
When you build this project, intentionally break things to learn debugging. Remove the client secret and see what error you get. Change the redirect URI and observe the error. Expire the token and try to use it. These debugging skills are what interviewers test for - not whether you can follow a setup tutorial.
Project 2: MFA Implementation
Extend your Keycloak setup with multi-factor authentication.
Configuring TOTP in Keycloak
- In the iam-lab realm, go to Authentication
- Edit the browser flow
- Set "OTP Form" to Required
- In Realm Settings - OTP Policy, configure:
- OTP Type: Time-Based (TOTP)
- Algorithm: SHA-256
- Number of digits: 6
- Token period: 30 seconds
Testing the MFA Flow
- Log out of any existing session
- Navigate to your demo app and click login
- After entering username/password, Keycloak will prompt for TOTP setup
- Scan the QR code with an authenticator app (Google Authenticator, Authy)
- Enter the code to complete authentication
Conditional MFA (Advanced)
Configure Keycloak to require MFA only for specific conditions:
- Create a custom authentication flow that branches based on user group
- Engineering group members: require MFA
- Finance group members: require MFA + additional step-up for sensitive resources
This mirrors real-world conditional access policies where MFA requirements vary by role, resource sensitivity, and risk level.
Project 3: Identity Governance - Access Certification
Build a basic access certification workflow. This project demonstrates that you understand governance, not just authentication.
Design
ACCESS CERTIFICATION WORKFLOW
==============================
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Generate │ │ Manager │ │ Execute │
│ Review │───>│ Reviews │───>│ Decisions │
│ Campaign │ │ Entitlements│ │ │
└─────────────┘ └──────────────┘ └──────────────┘
| | |
▼ ▼ ▼
Pull all users For each user: Approved: No change
and their - Show entitlements Revoked: Disable
entitlements - Approve/Revoke access
from Keycloak - Add justification Flagged: Escalate
Implementation Outline
# access_certification.py - Basic access review tool
import requests
import json
from datetime import datetime
KEYCLOAK_URL = "http://localhost:8080"
REALM = "iam-lab"
ADMIN_TOKEN = None # Obtained via admin API
def get_admin_token():
"""Get admin access token from Keycloak."""
response = requests.post(
f"{KEYCLOAK_URL}/realms/master/protocol/openid-connect/token",
data={
"grant_type": "password",
"client_id": "admin-cli",
"username": "admin",
"password": "admin"
}
)
return response.json()["access_token"]
def get_all_users(token):
"""Retrieve all users in the realm."""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
f"{KEYCLOAK_URL}/admin/realms/{REALM}/users",
headers=headers
)
return response.json()
def get_user_groups(token, user_id):
"""Get groups (roles) for a specific user."""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
f"{KEYCLOAK_URL}/admin/realms/{REALM}/users/{user_id}/groups",
headers=headers
)
return response.json()
def generate_certification_report(token):
"""Generate an access certification report."""
users = get_all_users(token)
report = {
"campaign_id": f"CERT-{datetime.now().strftime('%Y%m%d')}",
"generated": datetime.now().isoformat(),
"entries": []
}
for user in users:
groups = get_user_groups(token, user["id"])
for group in groups:
report["entries"].append({
"user": user["username"],
"user_id": user["id"],
"entitlement": group["name"],
"group_id": group["id"],
"decision": "PENDING", # APPROVE, REVOKE, FLAG
"reviewer": None,
"justification": None
})
return report
def execute_revocation(token, user_id, group_id):
"""Remove a user from a group (revoke access)."""
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(
f"{KEYCLOAK_URL}/admin/realms/{REALM}/users/{user_id}/groups/{group_id}",
headers=headers
)
return response.status_code == 204
# Usage
if __name__ == "__main__":
token = get_admin_token()
report = generate_certification_report(token)
print(json.dumps(report, indent=2))
print(f"\nTotal entitlements to review: {len(report['entries'])}")
This is a simplified version of what tools like SailPoint do. In production, access certification includes reviewer assignment logic, escalation workflows, risk scoring, and integration with dozens of target systems. But building this stripped-down version teaches you the core concepts - and gives you something tangible to discuss in interviews.
Project 4: OAuth 2.0 and OIDC Integration Patterns
Build multiple OAuth/OIDC integration patterns to understand when to use each one.
Pattern 1: Authorization Code with PKCE (SPA/Mobile)
// PKCE flow for single-page applications
// This is the recommended flow for any public client
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(new Uint8Array(digest));
}
async function startLogin() {
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store verifier for later use
sessionStorage.setItem('code_verifier', codeVerifier);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'spa-demo',
redirect_uri: 'http://localhost:3000/callback',
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: generateRandomState() // CSRF protection
});
window.location.href =
`http://localhost:8080/realms/iam-lab/protocol/openid-connect/auth?${params}`;
}
Pattern 2: Client Credentials (Machine-to-Machine)
# Machine-to-machine authentication
# No user involved - service authenticates as itself
import requests
def get_service_token():
"""Service authenticates using client credentials."""
response = requests.post(
"http://localhost:8080/realms/iam-lab/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": "backend-service",
"client_secret": "service-secret-here",
"scope": "api:read api:write"
}
)
token_data = response.json()
return token_data["access_token"]
def call_protected_api(token):
"""Call an API using the service token."""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
"http://localhost:4000/api/data",
headers=headers
)
return response.json()
Pattern 3: Token Validation Middleware
# API middleware that validates JWT tokens
# This is what the resource server does
import jwt
import requests
from functools import wraps
from flask import request, jsonify
# Fetch the IdP's public keys (JWKS)
JWKS_URL = "http://localhost:8080/realms/iam-lab/protocol/openid-connect/certs"
jwks_client = jwt.PyJWKClient(JWKS_URL)
def require_auth(f):
"""Decorator that validates JWT access tokens."""
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return jsonify({"error": "Missing or invalid token"}), 401
token = auth_header.split(" ")[1]
try:
# Get the signing key from JWKS
signing_key = jwks_client.get_signing_key_from_jwt(token)
# Verify and decode the token
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="demo-app",
issuer="http://localhost:8080/realms/iam-lab"
)
request.user = payload
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError as e:
return jsonify({"error": f"Invalid token: {str(e)}"}), 401
return decorated
# Usage
@app.route("/api/protected")
@require_auth
def protected_resource():
return jsonify({
"message": "Access granted",
"user": request.user["preferred_username"],
"roles": request.user.get("realm_access", {}).get("roles", [])
})
Project 5: Portfolio-Worthy IAM Projects
Beyond the core implementations above, here are projects that demonstrate broader IAM skills and look impressive in a portfolio:
1. Automated User Lifecycle with HR Integration
Build a pipeline that simulates HR events (hire, transfer, terminate) and automatically provisions/deprovisions users in Keycloak via the admin API.
What it demonstrates: JML lifecycle automation, API integration, SCIM concepts.
2. Multi-Tenant Identity Architecture
Configure Keycloak with multiple realms representing different tenants. Build an application that routes users to the correct realm based on their email domain.
What it demonstrates: Multi-tenancy, B2B SaaS identity patterns, identity routing.
3. Role-Based Access Control Dashboard
Build a web interface that displays users, roles, and permissions from Keycloak. Include features for role assignment, role hierarchy visualization, and SOD conflict detection.
What it demonstrates: IGA concepts, RBAC management, compliance awareness.
4. Identity Event Monitor
Build a service that consumes Keycloak event logs and generates real-time alerts for suspicious patterns: multiple failed logins, unusual login times, privilege escalation, account lockouts.
What it demonstrates: ITDR concepts, security monitoring, event processing.
5. Passkey Registration and Authentication
Implement WebAuthn/passkey registration and authentication in a web application. Use the browser's built-in WebAuthn API to create and verify passkeys.
What it demonstrates: Passwordless authentication, FIDO2, modern auth standards.
When presenting portfolio projects in interviews, focus on the decisions you made, not just the code you wrote. "I chose Authorization Code with PKCE instead of Implicit because PKCE prevents authorization code interception attacks, which is critical for public clients" shows understanding. "I used the openid-client library" does not.
Common Debugging Scenarios
Real IAM work involves constant debugging. Here are the issues you will encounter most frequently and how to approach them:
| Symptom | Likely Cause | Debug Approach |
|---|---|---|
| SAML "Invalid Signature" | Certificate mismatch | Compare SP metadata cert with IdP signing cert |
| OIDC "redirect_uri_mismatch" | Exact URI match failed | Check trailing slashes, http vs https, port numbers |
| "Invalid grant" on token exchange | Expired auth code | Auth codes are single-use and expire quickly (often 60s) |
| JWT "token expired" | Clock skew or short token lifetime | Check server time sync (NTP), adjust token lifetime |
| "Access denied" with valid token | Wrong audience or scope | Decode JWT and check aud, scope, and iss claims |
| SSO loop (redirect back and forth) | Session not created on SP | Check SP session configuration, cookie settings |
| SCIM provisioning fails silently | Attribute mapping error | Check SCIM server logs, validate schema compatibility |
What to Build Next
Once you have completed these projects, you have a working foundation. Next steps:
-
Deploy to cloud. Move your Keycloak instance to AWS/Azure/GCP. Configure HTTPS with proper certificates. This teaches you about production identity infrastructure.
-
Add a real application. Instead of demo apps, configure SSO for an open-source application like GitLab, Grafana, or Nextcloud.
-
Implement SCIM. Build a SCIM server that syncs users from Keycloak to a downstream application database.
-
Try a vendor platform. Sign up for Okta Developer, Auth0, or AWS Cognito free tiers and replicate your Keycloak setup. Understanding multiple platforms makes you more versatile.
The gap between "I understand IAM concepts" and "I have built IAM systems" is where careers are made. Close that gap with your hands on a keyboard.