---
description: "Multi-tenant Keycloak authentication theming with realm-specific design systems"
triggers:
- keycloak
- authentication
- login theme
- multi-tenant
- realm
- SSO
- PKCE
globs:
- "**/keycloak/**"
- "**/themes/**"
- "**/auth/**"
- "*.ftl"
---
# Keycloak Theming Skill
Multi-tenant Keycloak authentication theming with realm-specific design systems.
## Overview
This skill provides comprehensive guidance for implementing custom Keycloak themes across multiple realms with tenant-specific branding and design systems.
## Multi-Realm Configuration
### Realm Structure
```
Keycloak Instance
├── thelobbi (Realm)
│ ├── Theme: lobbi-theme
│ ├── Primary Color: #0066cc
│ ├── Logo: lobbi-logo.svg
│ └── Use Case: Main platform authentication
│
└── brooksidebi (Realm)
├── Theme: brookside-theme
├── Primary Color: #2c5282
├── Logo: brookside-logo.svg
└── Use Case: BI platform authentication
```
### Realm Configuration
**thelobbi Realm:**
```json
{
"realm": "thelobbi",
"displayName": "The Lobbi",
"displayNameHtml": "
The Lobbi
",
"loginTheme": "lobbi-theme",
"accountTheme": "lobbi-theme",
"adminTheme": "keycloak.v2",
"emailTheme": "lobbi-theme"
}
```
**brooksidebi Realm:**
```json
{
"realm": "brooksidebi",
"displayName": "Brookside BI",
"displayNameHtml": "Brookside BI
",
"loginTheme": "brookside-theme",
"accountTheme": "brookside-theme",
"adminTheme": "keycloak.v2",
"emailTheme": "brookside-theme"
}
```
## Custom Theme Structure
### Directory Layout
```
keycloak/
└── themes/
├── lobbi-theme/
│ ├── login/
│ │ ├── theme.properties
│ │ ├── resources/
│ │ │ ├── css/
│ │ │ │ ├── login.css
│ │ │ │ └── styles.css
│ │ │ ├── img/
│ │ │ │ ├── lobbi-logo.svg
│ │ │ │ ├── lobbi-icon.svg
│ │ │ │ └── background.jpg
│ │ │ └── js/
│ │ │ └── script.js
│ │ └── login.ftl
│ │
│ ├── account/
│ │ └── theme.properties
│ │
│ └── email/
│ └── theme.properties
│
└── brookside-theme/
└── [same structure as lobbi-theme]
```
### Theme Properties
**themes/lobbi-theme/login/theme.properties:**
```properties
parent=keycloak
import=common/keycloak
styles=css/login.css css/styles.css
stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css
meta=viewport==width=device-width,initial-scale=1
```
## Login Page Theming Templates (FTL)
### Base Login Template
**themes/lobbi-theme/login/login.ftl:**
```ftl
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
<#if section = "header">
${msg("loginAccountTitle")}
<#elseif section = "form">
<#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
#if>
#if>
@layout.registrationLayout>
```
### Custom Template with Realm-Specific Branding
**themes/lobbi-theme/login/template.ftl:**
```ftl
<#if properties.meta?has_content>
<#list properties.meta?split(' ') as meta>
#list>
#if>
${msg("loginTitle",(realm.displayName!''))}
<#if properties.stylesCommon?has_content>
<#list properties.stylesCommon?split(' ') as style>
#list>
#if>
<#if properties.styles?has_content>
<#list properties.styles?split(' ') as style>
#list>
#if>
<#nested "form">
<#if displayInfo>
<#nested "info">
#if>
```
## Realm-Specific CSS Variables
### Lobbi Theme Variables
**themes/lobbi-theme/login/resources/css/styles.css:**
```css
:root {
/* Brand Colors */
--lobbi-primary: #0066cc;
--lobbi-primary-dark: #0052a3;
--lobbi-primary-light: #3384d6;
--lobbi-secondary: #6c757d;
--lobbi-accent: #17a2b8;
/* Neutrals */
--lobbi-bg: #ffffff;
--lobbi-surface: #f8f9fa;
--lobbi-text: #212529;
--lobbi-text-secondary: #6c757d;
--lobbi-border: #dee2e6;
/* Status Colors */
--lobbi-success: #28a745;
--lobbi-error: #dc3545;
--lobbi-warning: #ffc107;
--lobbi-info: #17a2b8;
/* Typography */
--lobbi-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--lobbi-font-size-base: 16px;
--lobbi-font-size-large: 18px;
--lobbi-font-size-small: 14px;
/* Spacing */
--lobbi-spacing-xs: 0.5rem;
--lobbi-spacing-sm: 1rem;
--lobbi-spacing-md: 1.5rem;
--lobbi-spacing-lg: 2rem;
--lobbi-spacing-xl: 3rem;
/* Border Radius */
--lobbi-radius-sm: 4px;
--lobbi-radius-md: 8px;
--lobbi-radius-lg: 12px;
/* Shadows */
--lobbi-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--lobbi-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--lobbi-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
body.keycloak-theme.thelobbi-realm {
font-family: var(--lobbi-font-family);
background: linear-gradient(135deg, var(--lobbi-primary-light) 0%, var(--lobbi-primary) 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.kc-container {
width: 100%;
max-width: 480px;
padding: var(--lobbi-spacing-md);
}
.kc-content {
background: var(--lobbi-bg);
border-radius: var(--lobbi-radius-lg);
box-shadow: var(--lobbi-shadow-lg);
padding: var(--lobbi-spacing-xl);
}
.kc-brand {
text-align: center;
margin-bottom: var(--lobbi-spacing-lg);
}
.kc-brand img {
height: 48px;
width: auto;
}
/* Form Styles */
.kc-form-group {
margin-bottom: var(--lobbi-spacing-md);
}
.kc-label {
display: block;
font-size: var(--lobbi-font-size-small);
font-weight: 500;
color: var(--lobbi-text);
margin-bottom: var(--lobbi-spacing-xs);
}
.kc-input {
width: 100%;
padding: 0.75rem;
font-size: var(--lobbi-font-size-base);
border: 1px solid var(--lobbi-border);
border-radius: var(--lobbi-radius-md);
transition: border-color 0.2s, box-shadow 0.2s;
}
.kc-input:focus {
outline: none;
border-color: var(--lobbi-primary);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.kc-input[aria-invalid="true"] {
border-color: var(--lobbi-error);
}
/* Button Styles */
.kc-button {
width: 100%;
padding: 0.75rem;
font-size: var(--lobbi-font-size-base);
font-weight: 600;
border: none;
border-radius: var(--lobbi-radius-md);
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
}
.kc-button-primary {
background-color: var(--lobbi-primary);
color: white;
}
.kc-button-primary:hover {
background-color: var(--lobbi-primary-dark);
}
.kc-button-primary:active {
transform: translateY(1px);
}
/* Error Messages */
.kc-input-error-message {
display: block;
color: var(--lobbi-error);
font-size: var(--lobbi-font-size-small);
margin-top: var(--lobbi-spacing-xs);
}
/* Links */
a {
color: var(--lobbi-primary);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--lobbi-primary-dark);
text-decoration: underline;
}
```
### Brookside Theme Variables
**themes/brookside-theme/login/resources/css/styles.css:**
```css
:root {
/* Brand Colors - Brookside BI */
--brookside-primary: #2c5282;
--brookside-primary-dark: #1e3a5f;
--brookside-primary-light: #4a6fa5;
--brookside-secondary: #718096;
--brookside-accent: #805ad5;
/* Data Visualization Colors */
--brookside-chart-1: #4299e1;
--brookside-chart-2: #48bb78;
--brookside-chart-3: #ed8936;
--brookside-chart-4: #9f7aea;
/* Neutrals */
--brookside-bg: #f7fafc;
--brookside-surface: #ffffff;
--brookside-text: #1a202c;
--brookside-text-secondary: #718096;
--brookside-border: #e2e8f0;
/* Typography */
--brookside-font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
body.keycloak-theme.brooksidebi-realm {
font-family: var(--brookside-font-family);
background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 50%, #4a6fa5 100%);
}
/* Apply Brookside-specific styling... */
```
## JWT Claims and Theme Association
### Client Configuration for Theme Claims
```json
{
"clientId": "lobbi-web-app",
"protocolMappers": [
{
"name": "realm-name",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"config": {
"claim.name": "realm",
"claim.value": "thelobbi",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
},
{
"name": "theme-name",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"config": {
"claim.name": "theme",
"claim.value": "lobbi-theme",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
}
```
### Accessing Theme in Application
```typescript
// Example: Extract theme from JWT token
interface TokenPayload {
realm: string;
theme: string;
// ... other claims
}
function applyThemeFromToken(token: string) {
const payload = JSON.parse(atob(token.split('.')[1])) as TokenPayload;
// Apply theme dynamically
if (payload.theme === 'lobbi-theme') {
document.documentElement.setAttribute('data-theme', 'lobbi');
} else if (payload.theme === 'brookside-theme') {
document.documentElement.setAttribute('data-theme', 'brookside');
}
}
```
## Environment Variables Setup
### Keycloak Configuration
**.env.local:**
```bash
# Keycloak Base URL
NEXT_PUBLIC_KEYCLOAK_URL=https://auth.thelobbi.com
KEYCLOAK_URL=https://auth.thelobbi.com
# Lobbi Realm
NEXT_PUBLIC_KEYCLOAK_REALM_LOBBI=thelobbi
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID_LOBBI=lobbi-web-app
KEYCLOAK_CLIENT_SECRET_LOBBI=your-client-secret-here
# Brookside Realm
NEXT_PUBLIC_KEYCLOAK_REALM_BROOKSIDE=brooksidebi
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID_BROOKSIDE=brookside-web-app
KEYCLOAK_CLIENT_SECRET_BROOKSIDE=your-client-secret-here
# PKCE Configuration
NEXT_PUBLIC_KEYCLOAK_PKCE_ENABLED=true
NEXT_PUBLIC_KEYCLOAK_RESPONSE_TYPE=code
NEXT_PUBLIC_KEYCLOAK_SCOPE=openid profile email
# Redirect URIs
NEXT_PUBLIC_KEYCLOAK_REDIRECT_URI_LOBBI=https://app.thelobbi.com/auth/callback
NEXT_PUBLIC_KEYCLOAK_REDIRECT_URI_BROOKSIDE=https://bi.brooksideadvisory.com/auth/callback
```
### Next.js Authentication Configuration
**lib/keycloak.ts:**
```typescript
import Keycloak from 'keycloak-js';
interface KeycloakConfig {
realm: string;
clientId: string;
url: string;
}
export function getKeycloakConfig(tenant: 'lobbi' | 'brookside'): KeycloakConfig {
if (tenant === 'lobbi') {
return {
realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM_LOBBI!,
clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID_LOBBI!,
url: process.env.NEXT_PUBLIC_KEYCLOAK_URL!,
};
} else {
return {
realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM_BROOKSIDE!,
clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID_BROOKSIDE!,
url: process.env.NEXT_PUBLIC_KEYCLOAK_URL!,
};
}
}
export function initKeycloak(tenant: 'lobbi' | 'brookside') {
const config = getKeycloakConfig(tenant);
const keycloak = new Keycloak({
url: config.url,
realm: config.realm,
clientId: config.clientId,
});
return keycloak.init({
onLoad: 'login-required',
checkLoginIframe: false,
pkceMethod: 'S256', // PKCE enabled
});
}
```
## Docker Deployment
### Dockerfile for Custom Themes
```dockerfile
FROM quay.io/keycloak/keycloak:23.0
# Copy custom themes
COPY themes/lobbi-theme /opt/keycloak/themes/lobbi-theme
COPY themes/brookside-theme /opt/keycloak/themes/brookside-theme
# Set environment variables
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
# Production build
RUN /opt/keycloak/bin/kc.sh build
ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]
```
### Docker Compose
```yaml
version: '3.8'
services:
keycloak:
build: .
environment:
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${KC_DB_PASSWORD}
KC_HOSTNAME: auth.thelobbi.com
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
ports:
- "8080:8080"
depends_on:
- postgres
command: start --optimized
postgres:
image: postgres:15
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: ${KC_DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
## Testing Themes
### Theme Development Workflow
1. **Local Development:**
```bash
# Start Keycloak with theme watching
docker run -p 8080:8080 \
-v $(pwd)/themes:/opt/keycloak/themes \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:23.0 \
start-dev
```
2. **Clear Theme Cache:**
```bash
# In Keycloak admin console
Realm Settings > Themes > Clear Cache
```
3. **Test Different Realms:**
```bash
# Test Lobbi realm
http://localhost:8080/realms/thelobbi/account
# Test Brookside realm
http://localhost:8080/realms/brooksidebi/account
```
## Best Practices
1. **Consistency**: Maintain consistent branding across login, account, and email themes
2. **Accessibility**: Ensure WCAG AA compliance for all theme elements
3. **Performance**: Optimize images and CSS for fast loading
4. **Responsive**: Test themes on mobile, tablet, and desktop
5. **Security**: Never expose sensitive configuration in theme files
6. **Version Control**: Track theme changes with git
7. **Documentation**: Document realm-specific customizations
## Integration with Other Skills
- **design-styles**: Apply design system styles to Keycloak themes
- **css-generation**: Generate theme CSS from design tokens
- **component-patterns**: Use consistent components across app and auth pages
## Resources
- [Keycloak Themes Documentation](https://www.keycloak.org/docs/latest/server_development/#_themes)
- [FreeMarker Template Guide](https://freemarker.apache.org/docs/)
- [PKCE Flow Specification](https://oauth.net/2/pkce/)
- Theme Examples: `[[Resources/Keycloak/Theme-Examples]]`