Claude-skill-registry keycloak-admin
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/keycloak-admin" ~/.claude/skills/majiayu000-claude-skill-registry-keycloak-admin && rm -rf "$T"
manifest:
skills/data/keycloak-admin/SKILL.mdsource content
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 |
|---|---|---|
| GET | List all realms |
| POST | Create realm |
| GET/POST | Manage clients |
| GET/POST | Manage users |
| GET/POST | Manage roles |
| 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 |
|---|---|
| Keycloak configuration and themes |
| User management API |
| Token verification |
| Frontend Keycloak config |
| 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