Set up custom Keycloak themes with your organization's branding, implement SSL certificates, and deploy to production with hot reload development workflow and performance optimization.
Prerequisites
- Keycloak installation
- Node.js and npm
- NGINX web server
- Basic CSS/JavaScript knowledge
What this solves
Keycloak's default theme works for testing, but production deployments need custom branding that matches your organization's identity. This tutorial shows you how to create custom themes, configure SSL certificates, set up development workflows with hot reload, and deploy everything to production with proper security hardening.
Step-by-step configuration
Install theme development dependencies
Install Node.js and development tools needed for theme customization and asset compilation.
sudo apt update
sudo apt install -y nodejs npm git curl unzip
node --version
npm --version
Create custom theme directory structure
Set up the theme directory structure in your Keycloak installation. This follows Keycloak's theme provider architecture.
sudo mkdir -p /opt/keycloak/themes/custom-brand
sudo mkdir -p /opt/keycloak/themes/custom-brand/login
sudo mkdir -p /opt/keycloak/themes/custom-brand/account
sudo mkdir -p /opt/keycloak/themes/custom-brand/admin
sudo mkdir -p /opt/keycloak/themes/custom-brand/email
sudo mkdir -p /opt/keycloak/themes/custom-brand/welcome
sudo chown -R keycloak:keycloak /opt/keycloak/themes/custom-brand
Configure theme properties
Create the main theme configuration file that defines the theme structure and inheritance.
parent=keycloak
import=common/keycloak
styles=css/login.css css/custom.css
scripts=js/custom.js
locales=en,de,fr,es
Create login theme resources
Set up the login page theme with custom CSS and template overrides.
parent=base
import=common/keycloak
styles=css/login.css
scripts=js/login.js
locales=en,de,fr,es
Create CSS directory and custom styles
Add your organization's branding with custom CSS that overrides the default Keycloak styles.
sudo mkdir -p /opt/keycloak/themes/custom-brand/login/resources/css
sudo mkdir -p /opt/keycloak/themes/custom-brand/login/resources/js
sudo mkdir -p /opt/keycloak/themes/custom-brand/login/resources/img
/ Custom branding styles /
:root {
--brand-primary: #2563eb;
--brand-secondary: #1e40af;
--brand-accent: #3b82f6;
--text-color: #1f2937;
--background: #f9fafb;
}
.login-pf body {
background: var(--background);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.login-pf-page .card-pf {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
border: none;
}
#kc-header-wrapper {
background: var(--brand-primary);
padding: 2rem 0;
}
#kc-header-wrapper #kc-header {
color: white;
font-size: 1.5rem;
font-weight: 600;
}
.btn-primary {
background-color: var(--brand-primary);
border-color: var(--brand-primary);
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--brand-secondary);
border-color: var(--brand-secondary);
transform: translateY(-1px);
}
.form-control {
border-radius: 6px;
border: 1.5px solid #d1d5db;
padding: 0.75rem;
transition: border-color 0.2s;
}
.form-control:focus {
border-color: var(--brand-primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
#kc-logo {
width: 180px;
height: auto;
}
.alert-error {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626;
border-radius: 8px;
}
.alert-success {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #16a34a;
border-radius: 8px;
}
Add custom JavaScript functionality
Create JavaScript enhancements for better user experience and form validation.
// Custom login page enhancements
document.addEventListener('DOMContentLoaded', function() {
// Add loading state to login button
const loginForm = document.getElementById('kc-form-login');
const loginButton = document.getElementById('kc-login');
if (loginForm && loginButton) {
loginForm.addEventListener('submit', function() {
loginButton.disabled = true;
loginButton.innerHTML = ' Signing in...';
});
}
// Enhanced form validation
const usernameField = document.getElementById('username');
const passwordField = document.getElementById('password');
function validateField(field, minLength = 1) {
if (field.value.length < minLength) {
field.classList.add('error');
return false;
} else {
field.classList.remove('error');
return true;
}
}
if (usernameField) {
usernameField.addEventListener('blur', function() {
validateField(this);
});
}
if (passwordField) {
passwordField.addEventListener('blur', function() {
validateField(this, 1);
});
}
// Add custom analytics tracking
function trackEvent(event, data) {
if (typeof gtag !== 'undefined') {
gtag('event', event, data);
}
}
// Track login attempts
if (loginForm) {
loginForm.addEventListener('submit', function() {
trackEvent('login_attempt', {
'event_category': 'authentication',
'event_label': 'keycloak_login'
});
});
}
});
Create custom login template
Override the default login template with your custom branding and layout modifications.
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
<#if section = "header">
<div id="kc-header-wrapper">
<div id="kc-header">
<#if realm.displayNameHtml??>
${realm.displayNameHtml}
<#else>
${realm.displayName!"Sign In"}
</#if>
</div>
</div>
<#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<label for="username" class="${properties.kcLabelClass!}">
<#if !realm.loginWithEmailAllowed>${msg("username")}
<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}
<#else>${msg("email")}
</#if>
</label>
<#if usernameEditDisabled??>
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')" type="text" disabled />
<#else>
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')" type="text" autofocus autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
<#if messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span>
</#if>
</#if>
</div>
<div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<#if realm.rememberMe && !usernameEditDisabled??>
<div class="checkbox">
<label>
<#if login.rememberMe??>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
<#else>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
</#if>
</label>
</div>
</#if>
</div>
<div class="${properties.kcFormOptionsWrapperClass!}">
<#if realm.resetPasswordAllowed>
<span><a tabindex="5" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
</div>
</form>
</#if>
</div>
</div>
<#elseif section = "info">
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container">
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</div>
</#if>
</#if>
</@layout.registrationLayout>
Configure theme hot reload for development
Set up Keycloak for development mode with theme caching disabled for faster iteration.
# Development configuration
Database
db=postgres
db-username=keycloak
db-password=your-secure-password
db-url=jdbc:postgresql://localhost:5432/keycloak
HTTP/HTTPS
http-enabled=true
http-port=8080
https-port=8443
https-certificate-file=/opt/keycloak/certs/server.crt
https-certificate-key-file=/opt/keycloak/certs/server.key
Hostname
hostname=example.com
hostname-strict=false
hostname-strict-https=false
Cache (disable for development)
cache=local
cache-stack=local
theme-cache-themes=false
theme-cache-templates=false
theme-static-max-age=-1
Proxy
proxy=edge
proxy-headers=forwarded|x-forwarded-for|x-forwarded-proto|x-forwarded-host
Logging
log-level=INFO
log-console-color=true
Generate SSL certificates
Create SSL certificates for secure HTTPS connections. Use Let's Encrypt for production or self-signed for development.
sudo mkdir -p /opt/keycloak/certs
cd /opt/keycloak/certs
Generate self-signed certificate for development
sudo openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/OU=IT/CN=example.com"
Set proper permissions
sudo chown keycloak:keycloak /opt/keycloak/certs/*
sudo chmod 600 /opt/keycloak/certs/server.key
sudo chmod 644 /opt/keycloak/certs/server.crt
Configure reverse proxy integration
Set up NGINX reverse proxy to handle SSL termination and serve static theme assets efficiently.
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL Configuration
ssl_certificate /etc/ssl/certs/keycloak.crt;
ssl_certificate_key /etc/ssl/private/keycloak.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
# Theme assets caching
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Main proxy configuration
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
Enable the NGINX configuration
Activate the Keycloak site configuration and restart NGINX.
sudo ln -s /etc/nginx/sites-available/keycloak /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Configure theme in Keycloak admin console
Apply your custom theme to realms through the administrative interface.
# Start Keycloak
sudo systemctl start keycloak
sudo systemctl enable keycloak
Check status
sudo systemctl status keycloak
Set up production deployment configuration
Configure Keycloak for production with optimized settings and security hardening.
# Production configuration
Database
db=postgres
db-username=keycloak
db-password=${KC_DB_PASSWORD}
db-url=jdbc:postgresql://localhost:5432/keycloak
db-pool-initial-size=10
db-pool-min-size=10
db-pool-max-size=50
HTTPS only
http-enabled=false
https-port=8443
https-certificate-file=/opt/keycloak/certs/server.crt
https-certificate-key-file=/opt/keycloak/certs/server.key
Hostname
hostname=example.com
hostname-strict=true
hostname-strict-https=true
Cache (enable for production)
cache=ispn
cache-stack=kubernetes
theme-cache-themes=true
theme-cache-templates=true
theme-static-max-age=31536000
Proxy
proxy=edge
proxy-headers=forwarded|x-forwarded-for|x-forwarded-proto|x-forwarded-host
Logging
log-level=WARN
log-file=/var/log/keycloak/keycloak.log
log-file-max-size=10MB
log-file-max-backup-index=10
Performance
spi-theme-static-max-age=31536000
spi-theme-cache-themes=true
spi-theme-cache-templates=true
spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true
Security
spi-truststore-file-file=/opt/keycloak/truststore.p12
spi-truststore-file-password=${KC_TRUSTSTORE_PASSWORD}
spi-truststore-file-hostname-verification-policy=WILDCARD
Create theme deployment script
Automate theme deployment with a script that handles building, testing, and deployment.
#!/bin/bash
Keycloak theme deployment script
set -euo pipefail
THEME_NAME="custom-brand"
THEME_DIR="/opt/keycloak/themes/${THEME_NAME}"
BACKUP_DIR="/opt/keycloak/backups/themes"
KEYCLOAK_SERVICE="keycloak"
echo "Starting theme deployment for ${THEME_NAME}..."
Create backup
echo "Creating backup..."
sudo mkdir -p "${BACKUP_DIR}"
if [ -d "${THEME_DIR}" ]; then
sudo cp -r "${THEME_DIR}" "${BACKUP_DIR}/${THEME_NAME}-$(date +%Y%m%d-%H%M%S)"
fi
Validate CSS and JS
echo "Validating theme assets..."
find "${THEME_DIR}" -name "*.css" -exec csslint {} \;
find "${THEME_DIR}" -name "*.js" -exec jshint {} \;
Set proper permissions
echo "Setting permissions..."
sudo chown -R keycloak:keycloak "${THEME_DIR}"
sudo find "${THEME_DIR}" -type f -exec chmod 644 {} \;
sudo find "${THEME_DIR}" -type d -exec chmod 755 {} \;
Restart Keycloak if running
if sudo systemctl is-active --quiet "${KEYCLOAK_SERVICE}"; then
echo "Restarting Keycloak service..."
sudo systemctl restart "${KEYCLOAK_SERVICE}"
# Wait for service to be ready
echo "Waiting for Keycloak to be ready..."
timeout 120 bash -c 'until curl -f -s -k https://localhost:8443/health/ready; do sleep 2; done'
fi
echo "Theme deployment completed successfully!"
echo "Please update realm theme settings in the admin console."
Make deployment script executable
Set proper permissions and test the deployment script.
sudo chmod +x /opt/keycloak/scripts/deploy-theme.sh
sudo mkdir -p /opt/keycloak/backups/themes
Test the deployment
sudo /opt/keycloak/scripts/deploy-theme.sh
Verify your setup
Test your custom theme configuration and SSL setup with these verification commands.
# Check Keycloak service status
sudo systemctl status keycloak
Verify SSL configuration
openssl s_client -connect example.com:8443 -servername example.com
Test theme endpoints
curl -k -I https://example.com:8443/realms/master/login-actions/authenticate
curl -k -I https://example.com:8443/resources/custom-brand/login/css/login.css
Check logs for theme loading
sudo tail -f /var/log/keycloak/keycloak.log | grep -i theme
Verify NGINX proxy
curl -I https://example.com/auth/realms/master/.well-known/openid_configuration
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Theme not loading | Incorrect file permissions | sudo chown -R keycloak:keycloak /opt/keycloak/themes/ |
| CSS changes not visible | Theme caching enabled | Set theme-cache-themes=false in development |
| SSL certificate errors | Wrong certificate path | Verify paths in keycloak.conf and file permissions |
| 502 Bad Gateway | Keycloak not running | sudo systemctl start keycloak |
| Template parsing errors | Invalid FreeMarker syntax | Check template syntax and escape special characters |
| Asset 404 errors | Missing static files | Verify theme directory structure and file names |
Next steps
- Set up Keycloak SAML integration for enterprise SSO
- Configure OAuth2 integration with web applications
- Optimize NGINX reverse proxy configuration
- Implement multi-realm theme management
- Add multi-language support to custom themes
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Keycloak Custom Theme and Branding Installation Script
# Supports Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS, RHEL
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
KEYCLOAK_HOME=${KEYCLOAK_HOME:-"/opt/keycloak"}
THEME_NAME=${THEME_NAME:-"custom-brand"}
KEYCLOAK_USER=${KEYCLOAK_USER:-"keycloak"}
BRAND_PRIMARY=${BRAND_PRIMARY:-"#2563eb"}
BRAND_SECONDARY=${BRAND_SECONDARY:-"#1e40af"}
# Error handling
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
rm -rf "${KEYCLOAK_HOME}/themes/${THEME_NAME}" 2>/dev/null || true
}
trap cleanup ERR
# Print colored messages
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Usage message
usage() {
cat << EOF
Usage: $0 [OPTIONS]
Install and configure Keycloak custom themes and branding
Options:
-k, --keycloak-home PATH Keycloak installation path (default: /opt/keycloak)
-t, --theme-name NAME Theme name (default: custom-brand)
-u, --user USER Keycloak user (default: keycloak)
-p, --primary-color COLOR Primary brand color (default: #2563eb)
-s, --secondary-color COLOR Secondary brand color (default: #1e40af)
-h, --help Show this help message
Example:
$0 -k /opt/keycloak -t mycompany-theme -p "#ff6600"
EOF
exit 1
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-k|--keycloak-home)
KEYCLOAK_HOME="$2"
shift 2
;;
-t|--theme-name)
THEME_NAME="$2"
shift 2
;;
-u|--user)
KEYCLOAK_USER="$2"
shift 2
;;
-p|--primary-color)
BRAND_PRIMARY="$2"
shift 2
;;
-s|--secondary-color)
BRAND_SECONDARY="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
;;
esac
done
# Check if running as root or with sudo
check_privileges() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect Linux distribution
detect_distro() {
print_status "Detecting Linux distribution..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
print_success "Detected: $PRETTY_NAME"
else
print_error "Cannot detect distribution"
exit 1
fi
}
# Check prerequisites
check_prerequisites() {
print_status "Checking prerequisites..."
if [[ ! -d "$KEYCLOAK_HOME" ]]; then
print_error "Keycloak installation not found at $KEYCLOAK_HOME"
exit 1
fi
if ! id "$KEYCLOAK_USER" &>/dev/null; then
print_warning "User $KEYCLOAK_USER not found, will create it"
useradd -r -s /bin/false -d "$KEYCLOAK_HOME" "$KEYCLOAK_USER" 2>/dev/null || true
fi
print_success "Prerequisites check completed"
}
# Install development dependencies
install_dependencies() {
echo -e "${BLUE}[1/7]${NC} Installing theme development dependencies..."
$PKG_UPDATE
$PKG_INSTALL nodejs npm git curl unzip
# Verify installations
node --version
npm --version
print_success "Development dependencies installed"
}
# Create theme directory structure
create_theme_structure() {
echo -e "${BLUE}[2/7]${NC} Creating custom theme directory structure..."
local theme_path="${KEYCLOAK_HOME}/themes/${THEME_NAME}"
# Create main theme directories
mkdir -p "${theme_path}/login"
mkdir -p "${theme_path}/account"
mkdir -p "${theme_path}/admin"
mkdir -p "${theme_path}/email"
mkdir -p "${theme_path}/welcome"
# Create resource directories
mkdir -p "${theme_path}/login/resources/css"
mkdir -p "${theme_path}/login/resources/js"
mkdir -p "${theme_path}/login/resources/img"
# Set proper ownership and permissions
chown -R "${KEYCLOAK_USER}:${KEYCLOAK_USER}" "${theme_path}"
find "${theme_path}" -type d -exec chmod 755 {} \;
print_success "Theme directory structure created"
}
# Configure theme properties
configure_theme_properties() {
echo -e "${BLUE}[3/7]${NC} Configuring theme properties..."
local theme_path="${KEYCLOAK_HOME}/themes/${THEME_NAME}"
# Main theme properties
cat > "${theme_path}/theme.properties" << EOF
parent=keycloak
import=common/keycloak
styles=css/login.css css/custom.css
scripts=js/custom.js
locales=en,de,fr,es
EOF
# Login theme properties
cat > "${theme_path}/login/theme.properties" << EOF
parent=base
import=common/keycloak
styles=css/login.css
scripts=js/login.js
locales=en,de,fr,es
EOF
chown "${KEYCLOAK_USER}:${KEYCLOAK_USER}" "${theme_path}/theme.properties"
chown "${KEYCLOAK_USER}:${KEYCLOAK_USER}" "${theme_path}/login/theme.properties"
chmod 644 "${theme_path}/theme.properties"
chmod 644 "${theme_path}/login/theme.properties"
print_success "Theme properties configured"
}
# Create custom CSS styles
create_custom_styles() {
echo -e "${BLUE}[4/7]${NC} Creating custom CSS styles..."
local css_path="${KEYCLOAK_HOME}/themes/${THEME_NAME}/login/resources/css"
cat > "${css_path}/login.css" << EOF
/* Custom branding styles */
:root {
--brand-primary: ${BRAND_PRIMARY};
--brand-secondary: ${BRAND_SECONDARY};
--brand-accent: #3b82f6;
--text-color: #1f2937;
--background: #f9fafb;
}
.login-pf body {
background: var(--background);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
.login-pf-page .card-pf {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
border: none;
}
#kc-header-wrapper {
background: var(--brand-primary);
padding: 2rem 0;
}
#kc-header-wrapper #kc-header {
color: white;
font-size: 1.5rem;
font-weight: 600;
}
.btn-primary {
background-color: var(--brand-primary);
border-color: var(--brand-primary);
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--brand-secondary);
border-color: var(--brand-secondary);
transform: translateY(-1px);
}
.form-control {
border-radius: 6px;
border: 1.5px solid #d1d5db;
padding: 0.75rem;
transition: border-color 0.2s;
}
.form-control:focus {
border-color: var(--brand-primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
#kc-logo {
width: 180px;
height: auto;
}
.alert-error {
background-color: #fef2f2;
border-color: #fecaca;
color: #dc2626;
border-radius: 8px;
}
.alert-success {
background-color: #f0fdf4;
border-color: #bbf7d0;
color: #16a34a;
border-radius: 8px;
}
EOF
chown "${KEYCLOAK_USER}:${KEYCLOAK_USER}" "${css_path}/login.css"
chmod 644 "${css_path}/login.css"
print_success "Custom CSS styles created"
}
# Add custom JavaScript functionality
create_custom_scripts() {
echo -e "${BLUE}[5/7]${NC} Adding custom JavaScript functionality..."
local js_path="${KEYCLOAK_HOME}/themes/${THEME_NAME}/login/resources/js"
cat > "${js_path}/login.js" << EOF
// Custom login page enhancements
document.addEventListener('DOMContentLoaded', function() {
// Add loading state to login button
const loginForm = document.getElementById('kc-form-login');
const loginButton = document.getElementById('kc-login');
if (loginForm && loginButton) {
loginForm.addEventListener('submit', function() {
loginButton.disabled = true;
loginButton.innerHTML = 'Signing in...';
});
}
// Enhanced form validation
const inputs = document.querySelectorAll('.form-control');
inputs.forEach(function(input) {
input.addEventListener('blur', function() {
if (this.value.trim() === '') {
this.classList.add('error');
} else {
this.classList.remove('error');
}
});
});
// Auto-focus first input
const firstInput = document.querySelector('#username, #email');
if (firstInput) {
firstInput.focus();
}
});
EOF
chown "${KEYCLOAK_USER}:${KEYCLOAK_USER}" "${js_path}/login.js"
chmod 644 "${js_path}/login.js"
print_success "Custom JavaScript created"
}
# Set proper SELinux contexts (for RHEL-based systems)
configure_selinux() {
echo -e "${BLUE}[6/7]${NC} Configuring security contexts..."
if command -v semanage &>/dev/null && [[ $(getenforce 2>/dev/null) != "Disabled" ]]; then
print_status "Setting SELinux contexts for theme files..."
restorecon -R "${KEYCLOAK_HOME}/themes/" || true
fi
print_success "Security contexts configured"
}
# Verify installation
verify_installation() {
echo -e "${BLUE}[7/7]${NC} Verifying installation..."
local theme_path="${KEYCLOAK_HOME}/themes/${THEME_NAME}"
local errors=0
# Check directory structure
for dir in login account admin email welcome; do
if [[ ! -d "${theme_path}/${dir}" ]]; then
print_error "Missing directory: ${dir}"
((errors++))
fi
done
# Check required files
local required_files=(
"theme.properties"
"login/theme.properties"
"login/resources/css/login.css"
"login/resources/js/login.js"
)
for file in "${required_files[@]}"; do
if [[ ! -f "${theme_path}/${file}" ]]; then
print_error "Missing file: ${file}"
((errors++))
fi
done
# Check ownership
if [[ $(stat -c '%U' "${theme_path}") != "${KEYCLOAK_USER}" ]]; then
print_error "Incorrect ownership on theme directory"
((errors++))
fi
if [[ $errors -eq 0 ]]; then
print_success "Installation verification completed successfully"
echo
print_success "Custom theme '${THEME_NAME}' has been installed successfully!"
echo -e "${GREEN}Next steps:${NC}"
echo "1. Restart Keycloak service"
echo "2. Login to Keycloak Admin Console"
echo "3. Go to Realm Settings > Themes"
echo "4. Select '${THEME_NAME}' for Login Theme"
echo "5. Save and test the new theme"
else
print_error "Installation verification failed with ${errors} errors"
exit 1
fi
}
# Main execution
main() {
print_status "Starting Keycloak custom theme installation..."
check_privileges
detect_distro
check_prerequisites
install_dependencies
create_theme_structure
configure_theme_properties
create_custom_styles
create_custom_scripts
configure_selinux
verify_installation
print_success "Keycloak custom theme installation completed successfully!"
}
# Run main function
main "$@"
Review the script before running. Execute with: bash install.sh