Keycloak administration including realm management, client configuration, OAuth 2.0 setup, user management with custom attributes, role and group management, theme deployment, and token configuration. Activate for Keycloak Admin API operations, authentication setup, and identity provider configuration.
npx claudepluginhub markus41/claude --plugin lobbi-platform-managerThis skill is limited to using the following tools:
Comprehensive Keycloak administration for the keycloak-alpha multi-tenant MERN platform with OAuth 2.0 Authorization Code Flow.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
Comprehensive Keycloak administration for the keycloak-alpha multi-tenant MERN platform with OAuth 2.0 Authorization Code Flow.
Activate this skill when:
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"
| 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 |
# 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
}'
// 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
};
# 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"
]
}'
// 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`
};
# 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"
# 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
}]
}'
// 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;
}
}
# 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}"
# 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
}'
// 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 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"
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
# 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
# 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"
}'
// 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
# 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
}'
# 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"
}
}'
// 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();
});
}
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": ["+"]
}'
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`;
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": [...]
}'
Checklist:
GET /admin/realms/lobbi/users/{id}Solution:
| 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 |