Keycloak: Open-Source Identity and Access Management
Implementing enterprise IAM with Keycloak. User federation, SSO, OAuth2/OIDC, SAML, and integration with modern applications and microservices.
Keycloak: Open-Source Identity and Access Management
Keycloak is an open-source Identity and Access Management (IAM) solution that provides single sign-on, user federation, and identity brokering. It supports standard protocols like OAuth 2.0, OpenID Connect, and SAML 2.0, making it ideal for securing modern applications and microservices.
Keycloak Architecture
Core Components
Keycloak Architecture:
┌──────────────────────────────────────────────────────────────┐
│ Keycloak Server │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Realms │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Master │ │ Company │ │ Partner │ │ │
│ │ │ Realm │ │ Realm │ │ Realm │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Users & │ │ Clients │ │ Identity │ │
│ │ Groups │ │ (Applications)│ │ Providers │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Roles & │ │ Sessions │ │ Events & │ │
│ │ Permissions │ │ & Tokens │ │ Auditing │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────────────┘Key Concepts
| Concept | Description |
|---|---|
| Realm | Isolated tenant with its own users, clients, and settings |
| Client | Application that uses Keycloak for authentication |
| User Federation | Connect external user directories (LDAP, Active Directory) |
| Identity Provider | External login providers (Google, SAML IdP) |
| Roles | Define permissions that can be assigned to users |
| Groups | Organize users and assign role mappings |
Deployment
Kubernetes Deployment
# keycloak-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: identity
spec:
replicas: 2
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:24.0
args:
- start
- --hostname=auth.example.com
- --proxy-headers=xforwarded
- --http-enabled=true
env:
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: jdbc:postgresql://postgres:5432/keycloak
- name: KC_DB_USERNAME
valueFrom:
secretKeyRef:
name: keycloak-db
key: username
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-db
key: password
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_METRICS_ENABLED
value: "true"
- name: KEYCLOAK_ADMIN
valueFrom:
secretKeyRef:
name: keycloak-admin
key: username
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-admin
key: password
ports:
- name: http
containerPort: 8080
- name: management
containerPort: 9000
livenessProbe:
httpGet:
path: /health/live
port: management
initialDelaySeconds: 60
readinessProbe:
httpGet:
path: /health/ready
port: management
initialDelaySeconds: 30
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: identity
spec:
selector:
app: keycloak
ports:
- name: http
port: 8080
targetPort: 8080Terraform Configuration
# keycloak.tf
terraform {
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = "~> 4.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = var.keycloak_admin_username
password = var.keycloak_admin_password
url = var.keycloak_url
}
# Create realm
resource "keycloak_realm" "main" {
realm = var.realm_name
enabled = true
display_name = "${var.project} Realm"
# Login settings
login_with_email_allowed = true
duplicate_emails_allowed = false
reset_password_allowed = true
remember_me = true
registration_allowed = var.allow_registration
# Token settings
access_token_lifespan = "1h"
refresh_token_max_reuse = 0
sso_session_idle_timeout = "30m"
sso_session_max_lifespan = "10h"
# Password policy
password_policy = "length(12) and upperCase(1) and lowerCase(1) and digits(1) and specialChars(1) and notUsername"
# Brute force protection
brute_force_detection {
permanent_lockout = false
max_login_failures = 5
wait_increment_seconds = 60
quick_login_check_milli_seconds = 1000
minimum_quick_login_wait_seconds = 60
max_failure_wait_seconds = 900
failure_reset_time_seconds = 43200
}
# Email settings
smtp_server {
host = var.smtp_host
port = var.smtp_port
from = var.smtp_from
ssl = true
auth {
username = var.smtp_username
password = var.smtp_password
}
}
}
# Create client for web application
resource "keycloak_openid_client" "web" {
realm_id = keycloak_realm.main.id
client_id = "${var.project}-web"
name = "${var.project} Web Application"
enabled = true
access_type = "PUBLIC"
standard_flow_enabled = true
implicit_flow_enabled = false
direct_access_grants_enabled = false
valid_redirect_uris = var.web_redirect_uris
web_origins = var.web_origins
login_theme = var.login_theme
}
# Create client for API
resource "keycloak_openid_client" "api" {
realm_id = keycloak_realm.main.id
client_id = "${var.project}-api"
name = "${var.project} API"
enabled = true
access_type = "CONFIDENTIAL"
service_accounts_enabled = true
standard_flow_enabled = false
direct_access_grants_enabled = false
valid_redirect_uris = []
}
# Create roles
resource "keycloak_role" "admin" {
realm_id = keycloak_realm.main.id
name = "admin"
}
resource "keycloak_role" "user" {
realm_id = keycloak_realm.main.id
name = "user"
}
# Default role for new users
resource "keycloak_default_roles" "default" {
realm_id = keycloak_realm.main.id
default_roles = [keycloak_role.user.name]
}Client Integration
React/Next.js Integration
// lib/keycloak.ts
import Keycloak from 'keycloak-js';
const keycloakConfig = {
url: process.env.NEXT_PUBLIC_KEYCLOAK_URL!,
realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM!,
clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID!
};
let keycloak: Keycloak | null = null;
export const getKeycloak = (): Keycloak => {
if (!keycloak) {
keycloak = new Keycloak(keycloakConfig);
}
return keycloak;
};
// auth-context.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import Keycloak from 'keycloak-js';
import { getKeycloak } from './keycloak';
interface AuthContextType {
isAuthenticated: boolean;
isLoading: boolean;
token: string | null;
user: KeycloakUser | null;
login: () => void;
logout: () => void;
hasRole: (role: string) => boolean;
}
interface KeycloakUser {
id: string;
email: string;
name: string;
roles: string[];
}
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [keycloak, setKeycloak] = useState<Keycloak | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const kc = getKeycloak();
kc.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256'
})
.then((authenticated) => {
setKeycloak(kc);
setIsLoading(false);
if (authenticated) {
// Set up token refresh
setInterval(() => {
kc.updateToken(70).catch(() => {
console.log('Failed to refresh token');
});
}, 60000);
}
})
.catch((error) => {
console.error('Keycloak init failed:', error);
setIsLoading(false);
});
}, []);
const login = () => {
keycloak?.login();
};
const logout = () => {
keycloak?.logout({ redirectUri: window.location.origin });
};
const hasRole = (role: string): boolean => {
return keycloak?.hasRealmRole(role) || false;
};
const user: KeycloakUser | null = keycloak?.tokenParsed
? {
id: keycloak.tokenParsed.sub!,
email: keycloak.tokenParsed.email,
name: keycloak.tokenParsed.name,
roles: keycloak.tokenParsed.realm_access?.roles || []
}
: null;
return (
<AuthContext.Provider
value={{
isAuthenticated: !!keycloak?.authenticated,
isLoading,
token: keycloak?.token || null,
user,
login,
logout,
hasRole
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};Backend Token Validation
// middleware/auth.ts
import jwt, { JwtPayload } from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`,
cache: true,
cacheMaxAge: 600000
});
const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) {
callback(err);
} else {
const signingKey = key?.getPublicKey();
callback(null, signingKey);
}
});
};
interface TokenPayload extends JwtPayload {
realm_access?: { roles: string[] };
resource_access?: Record<string, { roles: string[] }>;
email: string;
preferred_username: string;
}
export const validateToken = (token: string): Promise<TokenPayload> => {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
algorithms: ['RS256']
},
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded as TokenPayload);
}
}
);
});
};
// Express middleware
export const requireAuth = async (
req: Request,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7);
try {
const decoded = await validateToken(token);
req.user = {
id: decoded.sub!,
email: decoded.email,
roles: decoded.realm_access?.roles || []
};
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
export const requireRole = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user?.roles || [];
const hasRole = roles.some(role => userRoles.includes(role));
if (!hasRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};User Federation
LDAP Configuration
# ldap.tf
resource "keycloak_ldap_user_federation" "ldap" {
realm_id = keycloak_realm.main.id
name = "Active Directory"
enabled = true
priority = 1
# Connection settings
connection_url = var.ldap_url
bind_dn = var.ldap_bind_dn
bind_credential = var.ldap_bind_password
users_dn = var.ldap_users_dn
connection_timeout = "5s"
read_timeout = "10s"
# Sync settings
batch_size_for_sync = 1000
full_sync_period = 604800 # Weekly
changed_sync_period = 86400 # Daily
import_enabled = true
sync_registrations = false
# User object settings
username_ldap_attribute = "sAMAccountName"
rdn_ldap_attribute = "cn"
uuid_ldap_attribute = "objectGUID"
user_object_classes = ["person", "organizationalPerson", "user"]
# Edit mode
edit_mode = "READ_ONLY"
}
# Map LDAP attributes to Keycloak
resource "keycloak_ldap_user_attribute_mapper" "email" {
realm_id = keycloak_realm.main.id
ldap_user_federation_id = keycloak_ldap_user_federation.ldap.id
name = "email"
user_model_attribute = "email"
ldap_attribute = "mail"
read_only = true
always_read_value_from_ldap = true
}
resource "keycloak_ldap_user_attribute_mapper" "first_name" {
realm_id = keycloak_realm.main.id
ldap_user_federation_id = keycloak_ldap_user_federation.ldap.id
name = "first name"
user_model_attribute = "firstName"
ldap_attribute = "givenName"
read_only = true
}
resource "keycloak_ldap_user_attribute_mapper" "last_name" {
realm_id = keycloak_realm.main.id
ldap_user_federation_id = keycloak_ldap_user_federation.ldap.id
name = "last name"
user_model_attribute = "lastName"
ldap_attribute = "sn"
read_only = true
}
# Map LDAP groups
resource "keycloak_ldap_group_mapper" "groups" {
realm_id = keycloak_realm.main.id
ldap_user_federation_id = keycloak_ldap_user_federation.ldap.id
name = "groups"
ldap_groups_dn = var.ldap_groups_dn
group_name_ldap_attribute = "cn"
group_object_classes = ["group"]
membership_ldap_attribute = "member"
membership_user_ldap_attribute = "dn"
mode = "READ_ONLY"
user_roles_retrieve_strategy = "LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"
drop_non_existing_groups_during_sync = false
}Custom Themes
Theme Structure
themes/custom-theme/
├── login/
│ ├── resources/
│ │ ├── css/
│ │ │ └── login.css
│ │ └── img/
│ │ └── logo.svg
│ ├── messages/
│ │ └── messages_en.properties
│ └── theme.properties
├── account/
│ └── theme.properties
└── email/
├── messages/
│ └── messages_en.properties
└── theme.propertiesLogin Theme CSS
/* themes/custom-theme/login/resources/css/login.css */
:root {
--pf-global--primary-color--100: #0066cc;
--pf-global--BackgroundColor--100: #ffffff;
}
.login-pf body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#kc-header-wrapper {
text-align: center;
padding: 40px 0;
}
#kc-logo {
width: 200px;
height: auto;
}
.card-pf {
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
max-width: 400px;
margin: 0 auto;
}
#kc-form-login {
padding: 30px;
}
.pf-c-button.pf-m-primary {
background-color: var(--pf-global--primary-color--100);
border-radius: 6px;
width: 100%;
}Key Takeaways
-
Use realms for tenancy: Each realm provides complete isolation
-
Secure token handling: Use PKCE for public clients, validate tokens properly
-
Federation for enterprise: Connect LDAP/AD instead of syncing users
-
Role-based access: Define granular roles and map to groups
-
High availability: Deploy multiple instances with shared database
-
Custom themes: Brand the login experience for your organisation
-
Monitor events: Enable event logging for security auditing
-
Regular updates: Keep Keycloak updated for security patches
Keycloak provides enterprise-grade IAM capabilities without vendor lock-in. Proper configuration of federation, roles, and security settings enables secure, scalable identity management.