Skip to content

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.

  1. Create a new realm called "iam-lab"
  2. Create two users: alice (Engineering department) and bob (Finance department)
  3. Set passwords for both users
  4. Create two groups: "engineering" and "finance"
  5. Assign alice to engineering and bob to finance

Configuring an OIDC Client

This simulates connecting a web application to your IdP for SSO.

  1. In the iam-lab realm, create a new client
  2. Client ID: "demo-app"
  3. Client protocol: openid-connect
  4. Access type: confidential
  5. Valid redirect URIs: http://localhost:3000/callback
  6. 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     |                       |
     |<-----------------|                       |
Tip

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

  1. In the iam-lab realm, go to Authentication
  2. Edit the browser flow
  3. Set "OTP Form" to Required
  4. 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

  1. Log out of any existing session
  2. Navigate to your demo app and click login
  3. After entering username/password, Keycloak will prompt for TOTP setup
  4. Scan the QR code with an authenticator app (Google Authenticator, Authy)
  5. Enter the code to complete authentication

Conditional MFA (Advanced)

Configure Keycloak to require MFA only for specific conditions:

  1. Create a custom authentication flow that branches based on user group
  2. Engineering group members: require MFA
  3. 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'])}")
Note

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.

Tip

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:

  1. Deploy to cloud. Move your Keycloak instance to AWS/Azure/GCP. Configure HTTPS with proper certificates. This teaches you about production identity infrastructure.

  2. Add a real application. Instead of demo apps, configure SSO for an open-source application like GitLab, Grafana, or Nextcloud.

  3. Implement SCIM. Build a SCIM server that syncs users from Keycloak to a downstream application database.

  4. 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.