NEW in v2.1.0 - Complete guide to integrating Keycloak SSO with your MCP server for production-ready authentication.
Overview
Keycloak provides enterprise-grade authentication with:
OpenID Connect / OAuth2 - Standards-compliant authentication
Single Sign-On (SSO) - One login for all applications
User Management - Centralized identity and access management
Social Login - Google, GitHub, Facebook, and more
Multi-Factor Authentication - TOTP, WebAuthn, SMS
Role & Group Management - Hierarchical organization
Architecture
Quick Start
Deploy Keycloak
Docker Compose
Kubernetes
Cloud Run
# docker-compose.yml
keycloak :
image : quay.io/keycloak/keycloak:latest
environment :
KEYCLOAK_ADMIN : admin
KEYCLOAK_ADMIN_PASSWORD : admin
KC_DB : postgres
KC_DB_URL : jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME : keycloak
KC_DB_PASSWORD : password
ports :
- "8080:8080"
command : start-dev
docker compose up -d keycloak
Access: http://localhost:8080/admin (admin/admin)
Run Setup Script
# Wait for Keycloak to be ready (60+ seconds)
sleep 60
# Run initialization script
python scripts/setup/setup_keycloak.py
# Save the client secret from output
KEYCLOAK_CLIENT_SECRET = abc123...
This script creates:
Realm : mcp-server-langgraph
Client : langgraph-client (confidential, OIDC)
Scopes : openid, profile, email, roles
Redirect URIs : Configured for your domain
Configure Environment
# .env or Kubernetes ConfigMap
AUTH_PROVIDER = keycloak
AUTH_MODE = session # or token
KEYCLOAK_SERVER_URL = https://sso.yourdomain.com
KEYCLOAK_REALM = mcp-server-langgraph
KEYCLOAK_CLIENT_ID = langgraph-client
KEYCLOAK_CLIENT_SECRET = your-client-secret-here
KEYCLOAK_VERIFY_SSL = true
KEYCLOAK_TIMEOUT = 30
KEYCLOAK_HOSTNAME = mcp-server-langgraph.yourdomain.com
Test Authentication
from mcp_server_langgraph.auth.keycloak import KeycloakClient
client = KeycloakClient()
# Authenticate user
result = await client.authenticate(
username = "alice" ,
password = "password123"
)
print ( f "Access Token: { result[ 'access_token' ] } " )
print ( f "Refresh Token: { result[ 'refresh_token' ] } " )
print ( f "Expires In: { result[ 'expires_in' ] } seconds" )
Manual Configuration
If you prefer to configure Keycloak manually via the admin console:
1. Create Realm
Open Keycloak admin console
Hover over realm dropdown (top left)
Click “Create Realm”
Name: mcp-server-langgraph
Click “Create”
2. Create Client
Go to Clients → Create client
Configure:
Client ID : langgraph-client
Client type : OpenID Connect
Client authentication : ON (confidential)
Click “Next”
Configure access:
Standard flow : ON (authorization code)
Direct access grants : ON (for testing)
Service accounts : OFF
Click “Next”
Set redirect URIs:
Valid redirect URIs: https://yourdomain.com/*
Web origins: https://yourdomain.com
Click “Save”
3. Get Client Secret
Go to Clients → langgraph-client
Click Credentials tab
Copy Client secret
Save to environment variables
Go to Clients → langgraph-client → Client scopes
Add scopes:
openid (required)
profile (user info)
email (email address)
roles (role mapping)
Click Add dedicated scope → Create roles scope if missing
5. Create Test User
Go to Users → Add user
Enter username: alice
Click “Create”
Go to Credentials tab
Click Set password
Enter password, set Temporary : OFF
Click “Save”
6. Assign Roles
Go to Users → Select alice
Click Role mapping tab
Assign roles (these map to OpenFGA):
Token Flow
Authorization Code Flow (Recommended)
For web applications:
// 1. Redirect user to Keycloak
const authUrl = ` https://sso.yourdomain.com/realms/mcp-server-langgraph/protocol/openid-connect/auth?
client_id = langgraph-client&
redirect_uri = https://yourapp.com/callback&
response_type = code&
scope = openid profile email roles` ;
window.location.href = authUrl ;
// 2. Keycloak redirects back with code
// https://yourapp.com/callback?code=abc123 & state = xyz
// 3. Exchange code for tokens
const tokens = await fetch ( 'https://sso.yourdomain.com/realms/mcp-server-langgraph/protocol/openid-connect/token' , {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams ({
grant_type: 'authorization_code',
code: 'abc123',
redirect_uri: 'https://yourapp.com/callback',
client_id: 'langgraph-client',
client_secret: 'your-secret'
})
});
const { access_token, refresh_token, expires_in } = await tokens.json ();
Direct Grant Flow (Testing Only)
For testing or CLI applications:
from mcp_server_langgraph.auth.keycloak import KeycloakClient
client = KeycloakClient()
## Direct authentication
result = await client.authenticate(
username = "alice" ,
password = "password123"
)
access_token = result[ 'access_token' ]
refresh_token = result[ 'refresh_token' ]
Never use Direct Grant in production! Use Authorization Code flow with PKCE for web/mobile apps.
Token Refresh
Refresh tokens have longer lifetime and can be used to get new access tokens:
## Refresh access token
new_tokens = await client.refresh_token ( refresh_token )
new_access_token = new_tokens['access_token']
new_refresh_token = new_tokens['refresh_token'] # Rotated!
Role Mapping to OpenFGA
NEW v2.1.0 : Automatic synchronization of Keycloak roles/groups to OpenFGA.
Configuration
Edit config/role_mappings.yaml:
## Simple 1:1 role mappings
simple_mappings :
admin : admin
user : member
viewer : viewer
## Regex-based group mappings
group_mappings :
- pattern : "/admins"
relation : admin
- pattern : "/users/.*"
relation : member
## Conditional mappings
conditional_mappings :
- condition :
attribute : email_verified
operator : ==
value : true
relation : member
## Role hierarchies
hierarchies :
- parent : admin
children : [ member , viewer ]
- parent : member
children : [ viewer ]
Automatic Sync
When user authenticates, roles are synced to OpenFGA:
from mcp_server_langgraph.auth.keycloak import sync_user_to_openfga
## Get user info from token
user_info = await client.get_user_info(access_token)
## Sync to OpenFGA
await sync_user_to_openfga(
user_id = user_info[ 'sub' ],
username = user_info[ 'preferred_username' ],
roles = user_info.get( 'roles' , []),
groups = user_info.get( 'groups' , []),
email_verified = user_info.get( 'email_verified' , False )
)
Manual Sync
Trigger sync for existing users:
## Sync single user
await sync_user_to_openfga(
user_id = "alice" ,
username = "alice" ,
roles =[ "admin" , "user" ] ,
groups =[ "/admins" , "/users/engineering" ]
)
## Sync all users (admin operation)
users = a wait client.get_all_users()
for user in users:
await sync_user_to_openfga(
user_id =u ser['id'],
username =u ser['username'],
roles =u ser.get('roles', []),
groups =u ser.get('groups', [])
)
Production Deployment
High Availability
Deploy Keycloak with 2+ replicas:
## values.yaml for Helm
replicaCount : 2
## Pod anti-affinity
affinity :
podAntiAffinity :
preferredDuringSchedulingIgnoredDuringExecution :
- weight : 100
podAffinityTerm :
labelSelector :
matchExpressions :
- key : app
operator : In
values : [ keycloak ]
topologyKey : kubernetes.io/hostname
## PostgreSQL backend (required!)
postgresql :
enabled : true
primary :
persistence :
enabled : true
size : 10Gi
SSL/TLS
Always use HTTPS in production:
ingress :
enabled : true
className : nginx
annotations :
cert-manager.io/cluster-issuer : letsencrypt-prod
hosts :
- host : sso.yourdomain.com
paths :
- path : /
pathType : Prefix
tls :
- secretName : keycloak-tls
hosts :
- sso.yourdomain.com
Configure app:
KEYCLOAK_SERVER_URL = https://sso.yourdomain.com
KEYCLOAK_VERIFY_SSL = true # REQUIRED!
## Kubernetes resources
resources :
requests :
cpu : 500m
memory : 1Gi
limits :
cpu : 2000m
memory : 2Gi
## JVM options
extraEnv :
- name : JAVA_OPTS
value : > -
-Xms1024m
-Xmx1536m
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
## Connection pooling
postgresql :
primary :
extendedConfiguration : |
max_connections = 200
shared_buffers = 256MB
Backup & Recovery
## Backup PostgreSQL (Keycloak database)
kubectl exec -n mcp-server-langgraph \
$( kubectl get pod -n mcp-server-langgraph -l app=postgresql -o jsonpath='{.items[0].metadata.name}' ) \
-- pg_dump -U keycloak keycloak > keycloak-backup.sql
## Restore
kubectl exec -i -n mcp-server-langgraph \
$( kubectl get pod -n mcp-server-langgraph -l app=postgresql -o jsonpath='{.items[0].metadata.name}' ) \
-- psql -U keycloak keycloak < keycloak-backup.sql
Monitoring
Health Checks
## Keycloak health
curl https://sso.yourdomain.com/health
## Readiness
curl https://sso.yourdomain.com/health/ready
## Liveness
curl https://sso.yourdomain.com/health/live
Metrics
Keycloak exposes Prometheus metrics:
## ServiceMonitor for Prometheus
apiVersion : monitoring.coreos.com/v1
kind : ServiceMonitor
metadata :
name : keycloak
spec :
selector :
matchLabels :
app : keycloak
endpoints :
- port : http
path : /metrics
``` text
Key metrics :
Login rate
rate(keycloak_logins_total[5m])
Failed logins
rate(keycloak_login_failures_total[5m])
Token creation rate
rate(keycloak_tokens_created_total[5m])
Active sessions
keycloak_sessions_active
#### Logging
Configure structured logging :
``` yaml
extraEnv :
- name : KC_LOG_LEVEL
value : "info"
- name : KC_LOG_CONSOLE_FORMAT
value : "json"
Troubleshooting
# Check Keycloak status
kubectl get pods -l app=keycloak
kubectl logs -l app=keycloak --tail=100
# Test connectivity
curl http://keycloak:8080/health
# Get correct secret from Keycloak
# Admin console → Clients → langgraph-client → Credentials
# Or via CLI
kubectl get secret keycloak-client-secret \
-o jsonpath='{.data.client-secret}' | base64 -d
Token verification failed
Check :
KEYCLOAK_SERVER_URL matches Keycloak issuer
Realm name is correct
Token not expired
JWKS endpoint accessible
# Debug token
import jwt
decoded = jwt.decode(token, options = { "verify_signature" : False })
print (decoded)
Development only :KEYCLOAK_VERIFY_SSL = false
Production : Fix SSL certificate or add CA cert
Next Steps
Production Ready : Keycloak provides enterprise-grade SSO for your MCP server!