Plain JavaScript Integration

Plain JavaScript Integration

This guide covers how to integrate Skycloak authentication into plain JavaScript applications without any framework dependencies.

Prerequisites

  • Modern web browser with ES6+ support
  • Skycloak cluster with configured realm and client
  • Basic web server to serve your application
  • Understanding of JavaScript promises and async/await

Quick Start

1. Include Keycloak Adapter

Download and include the Keycloak JavaScript adapter:

<!-- Option 1: CDN -->
<script src="https://cdn.jsdelivr.net/npm/keycloak-js@latest/dist/keycloak.min.js"></script>

<!-- Option 2: Local file -->
<script src="js/keycloak.min.js"></script>

2. Basic HTML Structure

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Skycloak JavaScript App</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="app">
      <header>
        <h1>My Application</h1>
        <div id="auth-status">
          <button id="login-btn" style="display: none;">Login</button>
          <div id="user-info" style="display: none;">
            <span id="username"></span>
            <button id="logout-btn">Logout</button>
          </div>
        </div>
      </header>

      <main id="content">
        <div id="loading">Initializing authentication...</div>
        <div id="public-content" style="display: none;">
          <h2>Public Content</h2>
          <p>This content is visible to everyone.</p>
        </div>
        <div id="protected-content" style="display: none;">
          <h2>Protected Content</h2>
          <p>This content requires authentication.</p>
        </div>
      </main>
    </div>

    <script src="js/keycloak.min.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

3. Initialize Keycloak

Create js/app.js:

// Keycloak configuration
const keycloakConfig = {
  url: 'https://your-cluster-id.app.skycloak.io',
  realm: 'your-realm',
  clientId: 'your-javascript-app',
};

// Initialize Keycloak instance
const keycloak = new Keycloak(keycloakConfig);

// Authentication state
let authenticated = false;

// Initialize Keycloak
async function initKeycloak() {
  try {
    authenticated = await keycloak.init({
      onLoad: 'check-sso',
      silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
      checkLoginIframe: false,
      pkceMethod: 'S256', // Enable PKCE for public clients
    });

    if (authenticated) {
      console.log('User is authenticated');
      showAuthenticatedUI();
      setupTokenRefresh();
    } else {
      console.log('User is not authenticated');
      showPublicUI();
    }
  } catch (error) {
    console.error('Failed to initialize Keycloak', error);
    showError('Authentication system unavailable');
  }
}

// Show authenticated UI
function showAuthenticatedUI() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('login-btn').style.display = 'none';
  document.getElementById('user-info').style.display = 'block';
  document.getElementById('username').textContent = keycloak.tokenParsed.preferred_username;
  document.getElementById('public-content').style.display = 'block';
  document.getElementById('protected-content').style.display = 'block';
}

// Show public UI
function showPublicUI() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('login-btn').style.display = 'block';
  document.getElementById('user-info').style.display = 'none';
  document.getElementById('public-content').style.display = 'block';
  document.getElementById('protected-content').style.display = 'none';
}

// Show error message
function showError(message) {
  document.getElementById('loading').textContent = message;
}

// Setup event listeners
document.addEventListener('DOMContentLoaded', () => {
  // Login button
  document.getElementById('login-btn').addEventListener('click', () => {
    keycloak.login();
  });

  // Logout button
  document.getElementById('logout-btn').addEventListener('click', () => {
    keycloak.logout();
  });

  // Initialize Keycloak
  initKeycloak();
});

// Token refresh
function setupTokenRefresh() {
  setInterval(() => {
    keycloak
      .updateToken(30)
      .then((refreshed) => {
        if (refreshed) {
          console.log('Token refreshed');
        }
      })
      .catch(() => {
        console.error('Failed to refresh token');
        keycloak.login();
      });
  }, 60000); // Check every minute
}

4. Create Silent Check SSO Page

Create silent-check-sso.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Silent SSO Check</title>
  </head>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>

Advanced Implementation

Complete Authentication Manager

// js/auth-manager.js
class AuthManager {
  constructor(config) {
    this.keycloak = new Keycloak(config);
    this.authenticated = false;
    this.userInfo = null;
    this.callbacks = {
      onAuthenticated: [],
      onLogout: [],
      onTokenExpired: [],
      onTokenRefreshed: [],
      onAuthError: [],
    };
  }

  // Initialize authentication
  async init(options = {}) {
    const defaultOptions = {
      onLoad: 'check-sso',
      checkLoginIframe: false,
      pkceMethod: 'S256',
      silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
    };

    try {
      this.authenticated = await this.keycloak.init({ ...defaultOptions, ...options });

      if (this.authenticated) {
        await this.loadUserInfo();
        this.setupTokenManagement();
        this.trigger('onAuthenticated', this.userInfo);
      }

      this.setupEventHandlers();
      return this.authenticated;
    } catch (error) {
      this.trigger('onAuthError', error);
      throw error;
    }
  }

  // Load user information
  async loadUserInfo() {
    try {
      this.userInfo = await this.keycloak.loadUserInfo();
      this.userInfo.roles = this.getRoles();
      this.userInfo.permissions = this.getPermissions();
      return this.userInfo;
    } catch (error) {
      console.error('Failed to load user info:', error);
      throw error;
    }
  }

  // Get user roles
  getRoles() {
    const token = this.keycloak.tokenParsed;
    if (!token) return { realm: [], client: {} };

    return {
      realm: token.realm_access?.roles || [],
      client: token.resource_access || {},
    };
  }

  // Get user permissions
  getPermissions() {
    // Extract permissions from token if available
    const token = this.keycloak.tokenParsed;
    return token?.permissions || [];
  }

  // Check if user has a specific role
  hasRole(role, clientId = null) {
    const roles = this.getRoles();

    if (clientId) {
      const clientRoles = roles.client[clientId]?.roles || [];
      return clientRoles.includes(role);
    }

    return roles.realm.includes(role);
  }

  // Check if user has any of the specified roles
  hasAnyRole(...roles) {
    const userRoles = this.getRoles().realm;
    return roles.some((role) => userRoles.includes(role));
  }

  // Check if user has all specified roles
  hasAllRoles(...roles) {
    const userRoles = this.getRoles().realm;
    return roles.every((role) => userRoles.includes(role));
  }

  // Login
  async login(options = {}) {
    return this.keycloak.login(options);
  }

  // Logout
  async logout(options = {}) {
    return this.keycloak.logout(options);
  }

  // Register
  async register(options = {}) {
    return this.keycloak.register(options);
  }

  // Get access token
  async getToken() {
    if (this.keycloak.isTokenExpired(5)) {
      await this.refreshToken();
    }
    return this.keycloak.token;
  }

  // Refresh token
  async refreshToken() {
    try {
      const refreshed = await this.keycloak.updateToken(5);
      if (refreshed) {
        this.trigger('onTokenRefreshed', this.keycloak.token);
      }
      return refreshed;
    } catch (error) {
      this.trigger('onTokenExpired');
      throw error;
    }
  }

  // Setup token management
  setupTokenManagement() {
    // Auto-refresh tokens
    setInterval(async () => {
      try {
        await this.refreshToken();
      } catch (error) {
        console.error('Token refresh failed:', error);
      }
    }, 30000); // Every 30 seconds

    // Refresh on token expiry
    this.keycloak.onTokenExpired = () => {
      this.trigger('onTokenExpired');
      this.refreshToken().catch(() => {
        this.login();
      });
    };
  }

  // Setup event handlers
  setupEventHandlers() {
    this.keycloak.onAuthSuccess = () => {
      console.log('Auth success');
    };

    this.keycloak.onAuthError = (error) => {
      console.error('Auth error:', error);
      this.trigger('onAuthError', error);
    };

    this.keycloak.onAuthLogout = () => {
      this.authenticated = false;
      this.userInfo = null;
      this.trigger('onLogout');
    };
  }

  // Event handling
  on(event, callback) {
    if (this.callbacks[event]) {
      this.callbacks[event].push(callback);
    }
  }

  trigger(event, data) {
    if (this.callbacks[event]) {
      this.callbacks[event].forEach((callback) => callback(data));
    }
  }

  // Make authenticated API call
  async fetch(url, options = {}) {
    const token = await this.getToken();

    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${token}`,
      },
    });
  }
}

// Export for use
window.AuthManager = AuthManager;

UI Components

// js/ui-components.js
class UIComponents {
  constructor(authManager) {
    this.auth = authManager;
  }

  // Create login button
  createLoginButton(options = {}) {
    const button = document.createElement('button');
    button.textContent = options.text || 'Login';
    button.className = options.className || 'btn btn-primary';
    button.onclick = () => this.auth.login(options.loginOptions);
    return button;
  }

  // Create logout button
  createLogoutButton(options = {}) {
    const button = document.createElement('button');
    button.textContent = options.text || 'Logout';
    button.className = options.className || 'btn btn-secondary';
    button.onclick = () => this.auth.logout(options.logoutOptions);
    return button;
  }

  // Create user profile widget
  createUserProfile() {
    const container = document.createElement('div');
    container.className = 'user-profile';

    if (this.auth.authenticated && this.auth.userInfo) {
      container.innerHTML = `
                <div class="user-avatar">
                    ${this.getInitials(this.auth.userInfo.name)}
                </div>
                <div class="user-details">
                    <div class="user-name">${this.auth.userInfo.name}</div>
                    <div class="user-email">${this.auth.userInfo.email}</div>
                </div>
            `;
    }

    return container;
  }

  // Create role-based element
  createRoleBasedElement(roles, element, options = {}) {
    const { requireAll = false, fallback = null } = options;

    const hasAccess = requireAll ? this.auth.hasAllRoles(...roles) : this.auth.hasAnyRole(...roles);

    if (hasAccess) {
      return element;
    } else if (fallback) {
      return fallback;
    } else {
      return document.createComment('Insufficient permissions');
    }
  }

  // Get user initials
  getInitials(name) {
    if (!name) return '?';
    return name
      .split(' ')
      .map((part) => part[0])
      .join('')
      .toUpperCase()
      .substring(0, 2);
  }

  // Show notification
  showNotification(message, type = 'info') {
    const notification = document.createElement('div');
    notification.className = `notification notification-${type}`;
    notification.textContent = message;

    document.body.appendChild(notification);

    setTimeout(() => {
      notification.classList.add('fade-out');
      setTimeout(() => notification.remove(), 300);
    }, 3000);
  }
}

API Client

// js/api-client.js
class APIClient {
  constructor(authManager, baseURL) {
    this.auth = authManager;
    this.baseURL = baseURL;
  }

  // Generic request method
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;

    try {
      const response = await this.auth.fetch(url, {
        ...options,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.statusText}`);
      }

      const contentType = response.headers.get('content-type');
      if (contentType && contentType.includes('application/json')) {
        return await response.json();
      }

      return await response.text();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }

  // GET request
  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(params).toString();
    const url = queryString ? `${endpoint}?${queryString}` : endpoint;
    return this.request(url, { method: 'GET' });
  }

  // POST request
  async post(endpoint, data) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // PUT request
  async put(endpoint, data) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  // DELETE request
  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }

  // PATCH request
  async patch(endpoint, data) {
    return this.request(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }
}

Complete Application Example

// js/main.js
(async function () {
  // Configuration
  const config = {
    keycloak: {
      url: 'https://your-cluster-id.app.skycloak.io',
      realm: 'your-realm',
      clientId: 'your-javascript-app',
    },
    api: {
      baseURL: 'https://api.example.com',
    },
  };

  // Initialize auth manager
  const authManager = new AuthManager(config.keycloak);
  const ui = new UIComponents(authManager);
  const api = new APIClient(authManager, config.api.baseURL);

  // Setup event handlers
  authManager.on('onAuthenticated', async (userInfo) => {
    console.log('User authenticated:', userInfo);
    await loadUserDashboard();
  });

  authManager.on('onLogout', () => {
    console.log('User logged out');
    showPublicContent();
  });

  authManager.on('onTokenRefreshed', () => {
    console.log('Token refreshed');
  });

  authManager.on('onAuthError', (error) => {
    console.error('Authentication error:', error);
    ui.showNotification('Authentication failed', 'error');
  });

  // Initialize authentication
  try {
    const authenticated = await authManager.init();

    if (authenticated) {
      await loadUserDashboard();
    } else {
      showPublicContent();
    }
  } catch (error) {
    console.error('Failed to initialize auth:', error);
    showErrorPage();
  }

  // Load user dashboard
  async function loadUserDashboard() {
    const container = document.getElementById('app-content');
    container.innerHTML = '';

    // Add user profile
    container.appendChild(ui.createUserProfile());

    // Add logout button
    container.appendChild(ui.createLogoutButton());

    // Load user-specific content
    try {
      const userData = await api.get('/user/profile');
      displayUserData(userData);
    } catch (error) {
      ui.showNotification('Failed to load user data', 'error');
    }

    // Show role-based content
    if (authManager.hasRole('admin')) {
      container.appendChild(createAdminPanel());
    }

    if (authManager.hasAnyRole('editor', 'moderator')) {
      container.appendChild(createContentManagement());
    }
  }

  // Show public content
  function showPublicContent() {
    const container = document.getElementById('app-content');
    container.innerHTML = `
            <h1>Welcome to Our Application</h1>
            <p>Please log in to access your dashboard.</p>
        `;

    container.appendChild(
      ui.createLoginButton({
        text: 'Sign In with Skycloak',
        className: 'btn btn-primary btn-lg',
      })
    );
  }

  // Show error page
  function showErrorPage() {
    const container = document.getElementById('app-content');
    container.innerHTML = `
            <h1>Error</h1>
            <p>Sorry, something went wrong. Please try again later.</p>
        `;
  }

  // Display user data
  function displayUserData(userData) {
    const dataContainer = document.createElement('div');
    dataContainer.className = 'user-data';
    dataContainer.innerHTML = `
            <h2>Your Profile</h2>
            <dl>
                <dt>Name</dt>
                <dd>${userData.name || 'Not set'}</dd>
                <dt>Email</dt>
                <dd>${userData.email}</dd>
                <dt>Member Since</dt>
                <dd>${new Date(userData.createdAt).toLocaleDateString()}</dd>
            </dl>
        `;
    document.getElementById('app-content').appendChild(dataContainer);
  }

  // Create admin panel
  function createAdminPanel() {
    const panel = document.createElement('div');
    panel.className = 'admin-panel';
    panel.innerHTML = `
            <h2>Admin Panel</h2>
            <button onclick="manageUsers()">Manage Users</button>
            <button onclick="viewSystemStats()">System Stats</button>
        `;
    return panel;
  }

  // Create content management
  function createContentManagement() {
    const panel = document.createElement('div');
    panel.className = 'content-management';
    panel.innerHTML = `
            <h2>Content Management</h2>
            <button onclick="createContent()">Create New</button>
            <button onclick="viewContent()">View All</button>
        `;
    return panel;
  }

  // Global functions for demo
  window.manageUsers = async () => {
    try {
      const users = await api.get('/admin/users');
      console.log('Users:', users);
      ui.showNotification('Loaded user list', 'success');
    } catch (error) {
      ui.showNotification('Failed to load users', 'error');
    }
  };

  window.viewSystemStats = async () => {
    try {
      const stats = await api.get('/admin/stats');
      console.log('Stats:', stats);
      ui.showNotification('Loaded system stats', 'success');
    } catch (error) {
      ui.showNotification('Failed to load stats', 'error');
    }
  };

  window.createContent = () => {
    ui.showNotification('Create content feature coming soon', 'info');
  };

  window.viewContent = async () => {
    try {
      const content = await api.get('/content');
      console.log('Content:', content);
      ui.showNotification('Loaded content list', 'success');
    } catch (error) {
      ui.showNotification('Failed to load content', 'error');
    }
  };
})();

Single Page Application (SPA) Support

// js/spa-router.js
class SPARouter {
  constructor(authManager) {
    this.auth = authManager;
    this.routes = new Map();
    this.currentRoute = null;

    // Listen for popstate events
    window.addEventListener('popstate', () => this.handleRoute());
  }

  // Register a route
  register(path, handler, options = {}) {
    this.routes.set(path, {
      handler,
      requiresAuth: options.requiresAuth || false,
      roles: options.roles || [],
    });
  }

  // Navigate to a route
  navigate(path) {
    window.history.pushState(null, '', path);
    this.handleRoute();
  }

  // Handle current route
  async handleRoute() {
    const path = window.location.pathname;
    const route = this.routes.get(path) || this.routes.get('*');

    if (!route) {
      console.error('No route found for:', path);
      return;
    }

    // Check authentication
    if (route.requiresAuth && !this.auth.authenticated) {
      // Store intended route
      sessionStorage.setItem('redirectPath', path);
      this.auth.login();
      return;
    }

    // Check roles
    if (route.roles.length > 0) {
      const hasAccess = route.roles.some((role) => this.auth.hasRole(role));
      if (!hasAccess) {
        this.navigate('/unauthorized');
        return;
      }
    }

    // Execute route handler
    this.currentRoute = path;
    await route.handler();
  }

  // Get current route
  getCurrentRoute() {
    return this.currentRoute;
  }
}

// Usage example
const router = new SPARouter(authManager);

// Register routes
router.register('/', () => {
  document.getElementById('content').innerHTML = '<h1>Home Page</h1>';
});

router.register(
  '/profile',
  async () => {
    const userData = await api.get('/user/profile');
    document.getElementById('content').innerHTML = `
        <h1>User Profile</h1>
        <p>Name: ${userData.name}</p>
        <p>Email: ${userData.email}</p>
    `;
  },
  { requiresAuth: true }
);

router.register(
  '/admin',
  () => {
    document.getElementById('content').innerHTML = '<h1>Admin Dashboard</h1>';
  },
  { requiresAuth: true, roles: ['admin'] }
);

router.register('/unauthorized', () => {
  document.getElementById('content').innerHTML = '<h1>Access Denied</h1>';
});

router.register('*', () => {
  document.getElementById('content').innerHTML = '<h1>404 - Page Not Found</h1>';
});

// Handle initial route
router.handleRoute();

CSS Styles

/* styles.css */
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f5f5f5;
}

#app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

header {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: background-color 0.3s;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-primary:hover {
  background-color: #0056b3;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.btn-secondary:hover {
  background-color: #545b62;
}

.user-profile {
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 20px;
  background-color: white;
  border-radius: 8px;
  margin-bottom: 20px;
}

.user-avatar {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background-color: #007bff;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

.notification {
  position: fixed;
  top: 20px;
  right: 20px;
  padding: 15px 20px;
  border-radius: 4px;
  color: white;
  animation: slideIn 0.3s ease-out;
  z-index: 1000;
}

.notification-info {
  background-color: #17a2b8;
}

.notification-success {
  background-color: #28a745;
}

.notification-error {
  background-color: #dc3545;
}

.notification.fade-out {
  animation: fadeOut 0.3s ease-out forwards;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes fadeOut {
  to {
    opacity: 0;
    transform: translateY(-20px);
  }
}

#loading {
  text-align: center;
  padding: 40px;
  color: #666;
}

.admin-panel,
.content-management {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  margin-top: 20px;
}

.admin-panel button,
.content-management button {
  margin-right: 10px;
}

Production Considerations

Security Best Practices

// js/security.js
class SecurityManager {
  constructor() {
    this.setupCSP();
    this.preventClickjacking();
    this.sanitizer = this.createSanitizer();
  }

  // Setup Content Security Policy
  setupCSP() {
    const meta = document.createElement('meta');
    meta.httpEquiv = 'Content-Security-Policy';
    meta.content = `
            default-src 'self';
            script-src 'self' https://cdn.jsdelivr.net;
            style-src 'self' 'unsafe-inline';
            connect-src 'self' https://*.skycloak.io https://api.example.com;
            frame-src 'self' https://*.skycloak.io;
            img-src 'self' data: https:;
        `.trim();
    document.head.appendChild(meta);
  }

  // Prevent clickjacking
  preventClickjacking() {
    if (window.top !== window.self) {
      window.top.location = window.self.location;
    }
  }

  // Create HTML sanitizer
  createSanitizer() {
    const config = {
      allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
      allowedAttributes: {
        a: ['href', 'title', 'target'],
      },
      allowedSchemes: ['http', 'https', 'mailto'],
    };

    return (html) => {
      // Basic sanitization - use DOMPurify in production
      const div = document.createElement('div');
      div.textContent = html;
      return div.innerHTML;
    };
  }

  // Validate URL
  isValidUrl(url) {
    try {
      const parsed = new URL(url);
      return ['http:', 'https:'].includes(parsed.protocol);
    } catch {
      return false;
    }
  }

  // Generate nonce for inline scripts
  generateNonce() {
    const array = new Uint8Array(16);
    crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array));
  }
}

Performance Optimization

// js/performance.js
class PerformanceOptimizer {
  constructor() {
    this.cache = new Map();
    this.pendingRequests = new Map();
  }

  // Cache API responses
  async cachedFetch(url, options = {}, ttl = 300000) {
    // 5 minutes default
    const cacheKey = `${url}-${JSON.stringify(options)}`;

    // Check cache
    const cached = this.cache.get(cacheKey);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.data;
    }

    // Check for pending request
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey);
    }

    // Make request
    const promise = fetch(url, options)
      .then((response) => response.json())
      .then((data) => {
        this.cache.set(cacheKey, {
          data,
          timestamp: Date.now(),
        });
        this.pendingRequests.delete(cacheKey);
        return data;
      })
      .catch((error) => {
        this.pendingRequests.delete(cacheKey);
        throw error;
      });

    this.pendingRequests.set(cacheKey, promise);
    return promise;
  }

  // Debounce function calls
  debounce(func, delay) {
    let timeoutId;
    return (...args) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => func(...args), delay);
    };
  }

  // Throttle function calls
  throttle(func, limit) {
    let inThrottle;
    return (...args) => {
      if (!inThrottle) {
        func.apply(this, args);
        inThrottle = true;
        setTimeout(() => (inThrottle = false), limit);
      }
    };
  }

  // Lazy load images
  lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]');
    const imageObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.removeAttribute('data-src');
          imageObserver.unobserve(img);
        }
      });
    });

    images.forEach((img) => imageObserver.observe(img));
  }

  // Preload critical resources
  preloadResources() {
    const resources = [
      { href: '/css/critical.css', as: 'style' },
      { href: '/js/keycloak.min.js', as: 'script' },
      {
        href: 'https://your-cluster-id.app.skycloak.io/realms/your-realm/.well-known/openid-configuration',
        as: 'fetch',
        crossOrigin: 'anonymous',
      },
    ];

    resources.forEach((resource) => {
      const link = document.createElement('link');
      link.rel = 'preload';
      Object.assign(link, resource);
      document.head.appendChild(link);
    });
  }
}

Troubleshooting

Common Issues and Solutions

// js/troubleshoot.js
class TroubleshootingHelper {
  constructor(authManager) {
    this.auth = authManager;
    this.issues = [];
  }

  // Run diagnostics
  async runDiagnostics() {
    console.log('Running authentication diagnostics...');

    // Check browser compatibility
    this.checkBrowserCompatibility();

    // Check third-party cookies
    await this.checkThirdPartyCookies();

    // Check Keycloak connectivity
    await this.checkKeycloakConnectivity();

    // Check token status
    this.checkTokenStatus();

    // Report results
    this.reportResults();
  }

  checkBrowserCompatibility() {
    const required = ['Promise', 'fetch', 'localStorage', 'sessionStorage'];
    const missing = required.filter((feature) => !(feature in window));

    if (missing.length > 0) {
      this.issues.push({
        type: 'error',
        message: `Browser missing required features: ${missing.join(', ')}`,
      });
    }
  }

  async checkThirdPartyCookies() {
    try {
      // Test if third-party cookies are enabled
      const testKey = 'cookie-test';
      const iframe = document.createElement('iframe');
      iframe.style.display = 'none';
      iframe.src = `${this.auth.keycloak.authServerUrl}/test-cookies.html`;

      document.body.appendChild(iframe);

      // Clean up
      setTimeout(() => iframe.remove(), 1000);
    } catch (error) {
      this.issues.push({
        type: 'warning',
        message: 'Third-party cookies may be disabled',
      });
    }
  }

  async checkKeycloakConnectivity() {
    try {
      const configUrl = `${this.auth.keycloak.authServerUrl}/realms/${this.auth.keycloak.realm}/.well-known/openid-configuration`;
      const response = await fetch(configUrl);

      if (!response.ok) {
        this.issues.push({
          type: 'error',
          message: `Keycloak server returned ${response.status}`,
        });
      }
    } catch (error) {
      this.issues.push({
        type: 'error',
        message: `Cannot connect to Keycloak: ${error.message}`,
      });
    }
  }

  checkTokenStatus() {
    if (this.auth.authenticated) {
      const token = this.auth.keycloak.tokenParsed;
      const now = Math.floor(Date.now() / 1000);
      const expiresIn = token.exp - now;

      if (expiresIn < 300) {
        // Less than 5 minutes
        this.issues.push({
          type: 'warning',
          message: `Token expires in ${Math.floor(expiresIn / 60)} minutes`,
        });
      }
    }
  }

  reportResults() {
    if (this.issues.length === 0) {
      console.log('✅ All diagnostics passed');
    } else {
      console.group('⚠️ Diagnostics found issues:');
      this.issues.forEach((issue) => {
        console[issue.type === 'error' ? 'error' : 'warn'](issue.message);
      });
      console.groupEnd();
    }
  }
}

// Usage
const troubleshooter = new TroubleshootingHelper(authManager);
// Run diagnostics on demand or when issues occur
window.runAuthDiagnostics = () => troubleshooter.runDiagnostics();

Next Steps