plugins/lobbi-platform-manager/skills/keycloak-admin/SKILL.md
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 skillsauth add markus41/claude keycloak-adminInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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": "[email protected]",
"email": "[email protected]",
"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 |
tools
Build Teams-native agents with the Teams SDK (formerly Teams AI Library v2) — App class, activity routing, adaptive cards, streaming, AI-generated labels, feedback, message extensions, Teams-as-MCP-server, and the bring-your-own-AI pattern with Agent Framework.
tools
Run agents on Microsoft Foundry (formerly Azure AI Foundry) Agent Service — prompt agents vs hosted agents, threads/runs and the Responses API, built-in tools (Bing grounding, code interpreter, file search, MCP, OpenAPI, A2A), connected agents, Entra agent identity, SDKs, and observability/evaluations.
tools
Build and host custom engine agents with the Microsoft 365 Agents SDK — AgentApplication, the Activity protocol, channel reach via Azure Bot Service, hosting Agent Framework or Semantic Kernel engines, and the Agents Toolkit/Playground workflow. Successor to the Bot Framework SDK.
tools
Design, govern, and extend Microsoft Copilot Studio agents — topics, generative orchestration, knowledge, tools and MCP, agent flows, autonomous triggers, publishing channels, Copilot Credits pricing, and solution-based ALM on Power Platform.