keycloak-admin
SKILL.md
Keycloak Admin Skill
Comprehensive Keycloak administration for the keycloak-alpha multi-tenant MERN platform with OAuth 2.0 Authorization Code Flow.
When to Use This Skill
Activate this skill when:
- Setting up Keycloak realms and clients
- Configuring OAuth 2.0 Authorization Code Flow
- Managing users with custom attributes (org_id)
- Deploying custom themes
- Troubleshooting authentication issues
- Configuring token lifetimes and session management
Keycloak Admin REST API
Authentication
Use the admin-cli client to obtain an access token:
# Get admin access token
TOKEN=$(curl -X POST "http://localhost:8080/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin" \
-d "password=admin" \
-d "grant_type=password" \
-d "client_id=admin-cli" | jq -r '.access_token')
# Use token in subsequent requests
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/master"
Key API Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/admin/realms |
GET | List all realms |
/admin/realms/{realm} |
POST | Create realm |
/admin/realms/{realm}/clients |
GET/POST | Manage clients |
/admin/realms/{realm}/users |
GET/POST | Manage users |
/admin/realms/{realm}/roles |
GET/POST | Manage roles |
/admin/realms/{realm}/groups |
GET/POST | Manage groups |
Realm Creation and Configuration
Create a New Realm
# Create realm with basic configuration
curl -X POST "http://localhost:8080/admin/realms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"realm": "lobbi",
"enabled": true,
"displayName": "Lobbi Platform",
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
"quickLoginCheckMilliSeconds": 1000,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 30,
"defaultSignatureAlgorithm": "RS256",
"revokeRefreshToken": false,
"refreshTokenMaxReuse": 0,
"accessTokenLifespan": 300,
"accessTokenLifespanForImplicitFlow": 900,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"offlineSessionIdleTimeout": 2592000,
"accessCodeLifespan": 60,
"accessCodeLifespanUserAction": 300,
"accessCodeLifespanLogin": 1800
}'
Configure Realm Settings
// In keycloak-alpha: services/keycloak-service/src/config/realm-config.js
export const realmDefaults = {
realm: process.env.KEYCLOAK_REALM || 'lobbi',
enabled: true,
displayName: 'Lobbi Platform',
// Security settings
sslRequired: 'external',
registrationAllowed: false,
loginWithEmailAllowed: true,
duplicateEmailsAllowed: false,
// Token lifespans (seconds)
accessTokenLifespan: 300, // 5 minutes
accessTokenLifespanForImplicitFlow: 900, // 15 minutes
ssoSessionIdleTimeout: 1800, // 30 minutes
ssoSessionMaxLifespan: 36000, // 10 hours
offlineSessionIdleTimeout: 2592000, // 30 days
// Login settings
resetPasswordAllowed: true,
editUsernameAllowed: false,
// Brute force protection
bruteForceProtected: true,
permanentLockout: false,
maxFailureWaitSeconds: 900,
minimumQuickLoginWaitSeconds: 60,
failureFactor: 30
};
Client Configuration for OAuth 2.0 Authorization Code Flow
Create Client
# Create client for Authorization Code Flow
curl -X POST "http://localhost:8080/admin/realms/lobbi/clients" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "lobbi-web-app",
"name": "Lobbi Web Application",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"http://localhost:3000/auth/callback",
"https://*.lobbi.com/auth/callback"
],
"webOrigins": [
"http://localhost:3000",
"https://*.lobbi.com"
],
"attributes": {
"pkce.code.challenge.method": "S256"
},
"defaultClientScopes": [
"email",
"profile",
"roles",
"web-origins"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access"
]
}'
Client Configuration in keycloak-alpha
// In: apps/web-app/src/config/keycloak.config.js
export const keycloakConfig = {
url: process.env.VITE_KEYCLOAK_URL || 'http://localhost:8080',
realm: process.env.VITE_KEYCLOAK_REALM || 'lobbi',
clientId: process.env.VITE_KEYCLOAK_CLIENT_ID || 'lobbi-web-app',
};
// OAuth 2.0 Authorization Code Flow with PKCE
export const authConfig = {
flow: 'standard',
pkceMethod: 'S256',
responseType: 'code',
scope: 'openid profile email roles',
// Redirect URIs
redirectUri: `${window.location.origin}/auth/callback`,
postLogoutRedirectUri: `${window.location.origin}/`,
// Token handling
checkLoginIframe: true,
checkLoginIframeInterval: 5,
onLoad: 'check-sso',
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`
};
Client Secret Management
# Get client secret
CLIENT_UUID=$(curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/clients?clientId=lobbi-web-app" \
| jq -r '.[0].id')
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/client-secret" \
| jq -r '.value'
# Regenerate client secret
curl -X POST -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/client-secret"
User Management with Custom Attributes
Create User with org_id
# Create user with custom org_id attribute
curl -X POST "http://localhost:8080/admin/realms/lobbi/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "john.doe@acme.com",
"email": "john.doe@acme.com",
"firstName": "John",
"lastName": "Doe",
"enabled": true,
"emailVerified": true,
"attributes": {
"org_id": ["org_acme"],
"tenant_name": ["ACME Corporation"]
},
"credentials": [{
"type": "password",
"value": "temp_password_123",
"temporary": true
}]
}'
User Service in keycloak-alpha
// In: services/user-service/src/controllers/user.controller.js
import axios from 'axios';
export class UserController {
async createUser(req, res) {
const { email, firstName, lastName, orgId } = req.body;
// Get admin token
const adminToken = await this.getAdminToken();
// Create user in Keycloak
const userData = {
username: email,
email,
firstName,
lastName,
enabled: true,
emailVerified: false,
attributes: {
org_id: [orgId],
created_by: [req.user.sub]
},
credentials: [{
type: 'password',
value: this.generateTemporaryPassword(),
temporary: true
}]
};
try {
const response = await axios.post(
`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`,
userData,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
// Extract user ID from Location header
const userId = response.headers.location.split('/').pop();
// Assign default roles
await this.assignRoles(userId, ['user'], adminToken);
// Send verification email
await this.sendVerificationEmail(userId, adminToken);
res.status(201).json({ userId, email });
} catch (error) {
console.error('User creation failed:', error.response?.data);
res.status(500).json({ error: 'Failed to create user' });
}
}
async getAdminToken() {
const response = await axios.post(
`${process.env.KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`,
new URLSearchParams({
username: process.env.KEYCLOAK_ADMIN_USER,
password: process.env.KEYCLOAK_ADMIN_PASSWORD,
grant_type: 'password',
client_id: 'admin-cli'
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
return response.data.access_token;
}
}
Query Users by org_id
# Search users by org_id attribute
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/users?q=org_id:org_acme"
# Get user with attributes
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/users/{user-id}"
Role and Group Management
Create Realm Roles
# Create organization-level roles
curl -X POST "http://localhost:8080/admin/realms/lobbi/roles" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "org_admin",
"description": "Organization Administrator",
"composite": false,
"clientRole": false
}'
curl -X POST "http://localhost:8080/admin/realms/lobbi/roles" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "org_user",
"description": "Organization User",
"composite": false,
"clientRole": false
}'
Assign Roles to User
// In: services/user-service/src/services/role.service.js
export class RoleService {
async assignRolesToUser(userId, roleNames, adminToken) {
// Get role definitions
const roles = await Promise.all(
roleNames.map(async (roleName) => {
const response = await axios.get(
`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/roles/${roleName}`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
return response.data;
})
);
// Assign roles to user
await axios.post(
`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}/role-mappings/realm`,
roles,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
}
async getUserRoles(userId, adminToken) {
const response = await axios.get(
`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}/role-mappings`,
{ headers: { Authorization: `Bearer ${adminToken}` } }
);
return response.data;
}
}
Create Groups for Organizations
# Create group for organization
curl -X POST "http://localhost:8080/admin/realms/lobbi/groups" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "org_acme",
"attributes": {
"org_id": ["org_acme"],
"org_name": ["ACME Corporation"]
}
}'
# Add user to group
GROUP_ID="..."
USER_ID="..."
curl -X PUT "http://localhost:8080/admin/realms/lobbi/users/$USER_ID/groups/$GROUP_ID" \
-H "Authorization: Bearer $TOKEN"
Theme Deployment
Theme Structure
keycloak-alpha/
└── services/
└── keycloak-service/
└── themes/
├── lobbi-base/
│ ├── login/
│ │ ├── theme.properties
│ │ ├── login.ftl
│ │ ├── register.ftl
│ │ └── resources/
│ │ ├── css/
│ │ │ └── login.css
│ │ ├── img/
│ │ │ └── logo.png
│ │ └── js/
│ │ └── login.js
│ ├── account/
│ └── email/
└── org-acme/
├── login/
│ ├── theme.properties (parent=lobbi-base)
│ └── resources/
│ ├── css/
│ │ └── custom.css
│ └── img/
│ └── org-logo.png
Theme Properties
# themes/lobbi-base/login/theme.properties
parent=keycloak
import=common/keycloak
styles=css/login.css
# Localization
locales=en,es,fr
# Custom properties
logo.url=/resources/img/logo.png
Deploy Theme
# Copy theme to Keycloak
docker cp themes/lobbi-base keycloak:/opt/keycloak/themes/
# Restart Keycloak to pick up new theme
docker restart keycloak
# Set theme for realm
curl -X PUT "http://localhost:8080/admin/realms/lobbi" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"loginTheme": "lobbi-base",
"accountTheme": "lobbi-base",
"emailTheme": "lobbi-base"
}'
Theme Customization per Organization
// In: services/keycloak-service/src/middleware/theme-mapper.js
export const themeMapper = {
org_acme: 'org-acme',
org_beta: 'org-beta',
default: 'lobbi-base'
};
export function getThemeForOrg(orgId) {
return themeMapper[orgId] || themeMapper.default;
}
// Apply theme dynamically via query parameter
// URL: http://localhost:8080/realms/lobbi/protocol/openid-connect/auth?kc_theme=org-acme
Token Configuration and Session Management
Token Lifetime Configuration
# Update token lifespans
curl -X PUT "http://localhost:8080/admin/realms/lobbi" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accessTokenLifespan": 300,
"accessTokenLifespanForImplicitFlow": 900,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"offlineSessionIdleTimeout": 2592000,
"accessCodeLifespan": 60,
"accessCodeLifespanUserAction": 300
}'
Custom Token Mapper for org_id
# Create protocol mapper to include org_id in token
CLIENT_UUID="..."
curl -X POST "http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/protocol-mappers/models" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "org_id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "org_id",
"claim.name": "org_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}'
Verify Token Claims
// In: services/api-gateway/src/middleware/auth.middleware.js
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
export async function verifyToken(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, getKey, {
audience: 'account',
issuer: `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}`,
algorithms: ['RS256']
}, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}
// Verify org_id claim exists
if (!decoded.org_id) {
return res.status(403).json({ error: 'Missing org_id claim' });
}
req.user = decoded;
next();
});
}
Common Troubleshooting
Issue: CORS Errors
Solution: Configure Web Origins in client settings
curl -X PUT "http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"webOrigins": ["+"]
}'
Issue: Invalid Redirect URI
Solution: Verify redirect URIs match exactly
// Check configured URIs
const redirectUris = [
'http://localhost:3000/auth/callback',
'https://app.lobbi.com/auth/callback'
];
// Ensure callback URL matches
const callbackUrl = `${window.location.origin}/auth/callback`;
Issue: Token Not Including Custom Claims
Solution: Verify protocol mapper is added to client scopes
# Check client scopes
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/admin/realms/lobbi/clients/$CLIENT_UUID/default-client-scopes"
# Add custom scope with org_id mapper
curl -X POST "http://localhost:8080/admin/realms/lobbi/client-scopes" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "org-scope",
"protocol": "openid-connect",
"protocolMappers": [...]
}'
Issue: User Cannot Login
Checklist:
- Verify user is enabled:
GET /admin/realms/lobbi/users/{id} - Check email is verified (if required)
- Verify password is not temporary
- Check realm login settings allow email login
- Review authentication flow configuration
Issue: Theme Not Applied
Solution:
- Verify theme is copied to Keycloak themes directory
- Restart Keycloak container
- Clear browser cache
- Check theme name in realm settings matches theme directory name
File Locations in keycloak-alpha
| Path | Purpose |
|---|---|
services/keycloak-service/ |
Keycloak configuration and themes |
services/user-service/ |
User management API |
services/api-gateway/src/middleware/auth.middleware.js |
Token verification |
apps/web-app/src/config/keycloak.config.js |
Frontend Keycloak config |
apps/web-app/src/hooks/useAuth.js |
Authentication hooks |
Best Practices
- Always use PKCE for Authorization Code Flow in SPAs
- Never expose client secrets in frontend code
- Validate org_id claim in every backend request
- Use short access token lifespans (5-15 minutes)
- Implement refresh token rotation for enhanced security
- Enable brute force protection in realm settings
- Use groups for organization-level permissions
- Version control themes in the repository
- Test theme changes in development realm first
- Monitor token usage and session metrics
Weekly Installs
4
Repository
lobbi-docs/claudeGitHub Stars
9
First Seen
Feb 27, 2026
Security Audits
Installed on
opencode4
gemini-cli4
github-copilot4
codex4
amp4
cline4