# ๐Ÿ” Keycloak Custom Login Implementation Guide ## ๐Ÿ“‹ **Table of Contents** 1. [Overview & Architecture](#overview--architecture) 2. [Keycloak Configuration](#keycloak-configuration) 3. [Environment Setup](#environment-setup) 4. [Server-Side Implementation](#server-side-implementation) 5. [Client-Side Implementation](#client-side-implementation) 6. [Authentication Flow](#authentication-flow) 7. [Session Management](#session-management) 8. [Security Implementation](#security-implementation) 9. [Middleware System](#middleware-system) 10. [Error Handling](#error-handling) 11. [Testing & Debugging](#testing--debugging) 12. [Production Considerations](#production-considerations) 13. [Troubleshooting](#troubleshooting) ## ๐Ÿ—๏ธ **Overview & Architecture** ### **Why Custom Login Instead of Keycloak Hosted Pages** **Benefits:** - โœ… **Complete UI Control**: Custom branding, responsive design, mobile optimization - โœ… **Better UX**: No redirects to external domains, seamless user experience - โœ… **Integration**: Easy integration with existing Nuxt/Vue components - โœ… **Mobile Compatibility**: Full control over mobile browser behavior - โœ… **Performance**: No external redirects, faster login experience **Trade-offs:** - โš ๏ธ **More Complex**: Must handle security, session management, token validation - โš ๏ธ **Security Responsibility**: Must implement proper rate limiting, CSRF protection - โš ๏ธ **Maintenance**: More code to maintain vs. Keycloak's hosted pages ### **Architecture Overview** ```mermaid graph TD A[Client Browser] --> B[Nuxt App] B --> C[Custom Login Page] C --> D[Server API Route] D --> E[Keycloak Token Endpoint] E --> F[Return Access Token] F --> D D --> G[Create Encrypted Session] G --> H[Set HTTP-Only Cookie] H --> B B --> I[Authenticated App] ``` ### **Tech Stack** - **Frontend**: Nuxt 3 + Vue 3 + Vuetify 3 - **Backend**: Nuxt 3 Server Routes - **Authentication**: Keycloak (Resource Owner Password Credentials flow) - **Session**: Encrypted server-side session storage - **Security**: Rate limiting, input validation, CSRF protection ## ๐Ÿ”ง **Keycloak Configuration** ### **1. Client Configuration** In Keycloak Admin Console: ```javascript // Client Settings { clientId: "monacousa-portal", clientType: "confidential", // Important: Must be confidential standardFlowEnabled: false, // Disable standard flow directAccessGrantsEnabled: true, // Enable direct access grants serviceAccountsEnabled: false, publicClient: false, // Valid Redirect URIs (for OAuth fallback if needed) redirectUris: [ "https://yourdomain.com/auth/callback", "http://localhost:3000/auth/callback" // Dev only ], // Root URL rootUrl: "https://yourdomain.com", adminUrl: "https://yourdomain.com", baseUrl: "https://yourdomain.com", // Advanced Settings accessTokenLifespan: "15 minutes", ssoSessionIdleTimeout: "30 minutes", ssoSessionMaxLifespan: "12 hours" } ``` ### **2. Required Scopes** ```javascript // Default Client Scopes (include these) [ "openid", // OpenID Connect "profile", // User profile information "email", // Email address "roles", // User roles/groups "groups" // User groups (if using) ] ``` ### **3. User Groups/Roles Setup** ```javascript // Example Group Structure { groups: [ { name: "admin", description: "System administrators" }, { name: "board", description: "Board members" }, { name: "user", description: "Regular users" } ] } ``` ### **4. Keycloak Client Secret** ```bash # In Keycloak Admin Console: # Clients โ†’ [Your Client] โ†’ Credentials โ†’ Client Secret # Copy this secret for environment variables ``` ## ๐ŸŒ **Environment Setup** ### **Environment Variables** ```bash # .env # Keycloak Configuration NUXT_KEYCLOAK_ISSUER=https://auth.yourdomain.com/realms/your-realm NUXT_KEYCLOAK_CLIENT_ID=your-client-id NUXT_KEYCLOAK_CLIENT_SECRET=your-client-secret NUXT_KEYCLOAK_CALLBACK_URL=https://yourdomain.com/auth/callback # Session Security NUXT_SESSION_SECRET=your-48-character-session-secret-key-here-123456 NUXT_ENCRYPTION_KEY=your-32-character-encryption-key-here12 # Public Configuration NUXT_PUBLIC_DOMAIN=yourdomain.com ``` ### **Nuxt Configuration** ```typescript // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { // Private (server-side only) keycloak: { issuer: process.env.NUXT_KEYCLOAK_ISSUER, clientId: process.env.NUXT_KEYCLOAK_CLIENT_ID, clientSecret: process.env.NUXT_KEYCLOAK_CLIENT_SECRET, callbackUrl: process.env.NUXT_KEYCLOAK_CALLBACK_URL }, sessionSecret: process.env.NUXT_SESSION_SECRET, encryptionKey: process.env.NUXT_ENCRYPTION_KEY, // Public (client-side accessible) public: { domain: process.env.NUXT_PUBLIC_DOMAIN } }, // SSR Configuration for auth ssr: false, // or configure properly for SSR // Security headers nitro: { experimental: { wasm: true } } }) ``` ## ๐Ÿ–ฅ๏ธ **Server-Side Implementation** ### **1. Direct Login API Route** ```typescript // server/api/auth/direct-login.post.ts export default defineEventHandler(async (event) => { try { const { username, password, rememberMe } = await readBody(event); const config = useRuntimeConfig(); // Input validation if (!username || !password) { throw createError({ statusCode: 400, statusMessage: 'Username and password are required' }); } // Rate limiting const clientIP = getClientIP(event); if (!checkRateLimit(clientIP)) { throw createError({ statusCode: 429, statusMessage: 'Too many login attempts' }); } // Direct authentication with Keycloak const tokenResponse = await fetch(`${config.keycloak.issuer}/protocol/openid-connect/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'YourApp/1.0' }, body: new URLSearchParams({ grant_type: 'password', client_id: config.keycloak.clientId, client_secret: config.keycloak.clientSecret, username, password, scope: 'openid email profile roles' }) }); if (!tokenResponse.ok) { throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' }); } const tokens = await tokenResponse.json(); // Get user info from Keycloak const userResponse = await fetch(`${config.keycloak.issuer}/protocol/openid-connect/userinfo`, { headers: { 'Authorization': `Bearer ${tokens.access_token}` } }); const userInfo = await userResponse.json(); // Extract user groups and determine tier const groups = extractUserGroups(tokens, userInfo); const userTier = determineTier(groups); // Create session const sessionData = { user: { id: userInfo.sub, email: userInfo.email, name: userInfo.name, firstName: userInfo.given_name, lastName: userInfo.family_name, tier: userTier, groups: groups }, tokens: { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + (tokens.expires_in * 1000) }, rememberMe: !!rememberMe, createdAt: Date.now(), lastActivity: Date.now() }; // Create encrypted session cookie const sessionManager = createSessionManager(); const cookieString = sessionManager.createSession(sessionData, !!rememberMe); // Set secure cookie const maxAge = !!rememberMe ? 60 * 60 * 24 * 30 : 60 * 60 * 24 * 7; const isProduction = process.env.NODE_ENV === 'production'; setCookie(event, 'your-session-name', cookieString.split('=')[1], { httpOnly: true, secure: isProduction, sameSite: 'lax', maxAge, path: '/' }); return { success: true, redirectTo: '/dashboard', user: { email: userInfo.email, name: userInfo.name, tier: userTier } }; } catch (error) { // Error handling throw createError({ statusCode: error.statusCode || 500, statusMessage: error.statusMessage || 'Authentication failed' }); } }); ``` ### **2. Session Validation API** ```typescript // server/api/auth/session.get.ts export default defineEventHandler(async (event) => { try { const sessionManager = createSessionManager(); const sessionData = sessionManager.getSession(event); if (!sessionData || !sessionData.user) { return { authenticated: false, user: null }; } // Check if token needs refresh if (sessionData.tokens.expiresAt < Date.now() + 60000) { // 1 minute buffer try { const refreshedTokens = await refreshKeycloakToken(sessionData.tokens.refreshToken); // Update session with new tokens sessionData.tokens = { accessToken: refreshedTokens.access_token, refreshToken: refreshedTokens.refresh_token, expiresAt: Date.now() + (refreshedTokens.expires_in * 1000) }; sessionManager.updateSession(event, sessionData); } catch (refreshError) { console.warn('Token refresh failed:', refreshError); // Session is invalid, force logout return { authenticated: false, user: null }; } } // Update last activity sessionData.lastActivity = Date.now(); sessionManager.updateSession(event, sessionData); return { authenticated: true, user: sessionData.user }; } catch (error) { console.error('Session validation error:', error); return { authenticated: false, user: null }; } }); ``` ### **3. Session Management Utilities** ```typescript // server/utils/session.ts import crypto from 'crypto'; interface SessionData { user: User; tokens: { accessToken: string; refreshToken: string; expiresAt: number; }; rememberMe: boolean; createdAt: number; lastActivity: number; } export const createSessionManager = () => { const config = useRuntimeConfig(); const encryptData = (data: any): string => { const cipher = crypto.createCipher('aes-256-cbc', config.encryptionKey); let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; }; const decryptData = (encryptedData: string): any => { try { const decipher = crypto.createDecipher('aes-256-cbc', config.encryptionKey); let decrypted = decipher.update(encryptedData, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } catch (error) { return null; } }; const createSession = (sessionData: SessionData, rememberMe: boolean): string => { const sessionId = crypto.randomUUID(); const encryptedData = encryptData(sessionData); // Store in server-side storage (memory, database, Redis, etc.) serverSessionStorage.set(sessionId, encryptedData); return `session-id=${sessionId}`; }; const getSession = (event: any): SessionData | null => { const sessionId = getCookie(event, 'your-session-name'); if (!sessionId) return null; const encryptedData = serverSessionStorage.get(sessionId); if (!encryptedData) return null; return decryptData(encryptedData); }; return { createSession, getSession, updateSession: (event: any, sessionData: SessionData) => { const sessionId = getCookie(event, 'your-session-name'); if (sessionId) { const encryptedData = encryptData(sessionData); serverSessionStorage.set(sessionId, encryptedData); } }, destroySession: (event: any) => { const sessionId = getCookie(event, 'your-session-name'); if (sessionId) { serverSessionStorage.delete(sessionId); deleteCookie(event, 'your-session-name'); } } }; }; ``` ## ๐ŸŽจ **Client-Side Implementation** ### **1. Authentication Composable** ```typescript // composables/useAuth.ts export const useAuth = () => { // Use useState for SSR compatibility const user = useState('auth.user', () => null); const isAuthenticated = computed(() => !!user.value); const loading = ref(false); const error = ref(null); // Tier-based computed properties const userTier = computed(() => user.value?.tier || 'user'); const isUser = computed(() => user.value?.tier === 'user'); const isBoard = computed(() => user.value?.tier === 'board'); const isAdmin = computed(() => user.value?.tier === 'admin'); // Login method const login = async (credentials: LoginCredentials) => { loading.value = true; error.value = null; try { const response = await $fetch('/api/auth/direct-login', { method: 'POST', body: credentials, timeout: 30000 }); if (response.success) { // Wait for cookie to be set await new Promise(resolve => setTimeout(resolve, 200)); // Verify session was created properly let sessionSuccess = false; let attempts = 0; const maxAttempts = 3; while (!sessionSuccess && attempts < maxAttempts) { attempts++; sessionSuccess = await checkAuth(); if (!sessionSuccess && attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, 500)); } } if (sessionSuccess) { return { success: true, redirectTo: response.redirectTo || '/dashboard' }; } } return { success: false, error: 'Login failed' }; } catch (err: any) { const errorMessage = getErrorMessage(err); error.value = errorMessage; return { success: false, error: errorMessage }; } finally { loading.value = false; } }; // Check authentication status const checkAuth = async (): Promise => { try { const response = await $fetch('/api/auth/session'); if (response.authenticated && response.user) { user.value = response.user; return true; } else { user.value = null; return false; } } catch (err) { user.value = null; return false; } }; // Logout method const logout = async () => { try { await $fetch('/api/auth/logout', { method: 'POST' }); user.value = null; await navigateTo('/login'); } catch (err) { user.value = null; await navigateTo('/login'); } }; return { user: readonly(user), isAuthenticated, loading: readonly(loading), error: readonly(error), userTier, isUser, isBoard, isAdmin, login, logout, checkAuth }; }; ``` ### **2. Custom Login Page** ```vue ``` ### **3. Auth Plugin (Initialize Auth State)** ```typescript // plugins/01.auth-check.client.ts export default defineNuxtPlugin(async () => { const { checkAuth } = useAuth(); // Check authentication status on app startup await checkAuth(); }); ``` ## ๐Ÿ›ก๏ธ **Security Implementation** ### **1. Rate Limiting** ```typescript // server/utils/security.ts const loginAttempts = new Map(); const blockedIPs = new Map(); export const checkRateLimit = (ip: string): { allowed: boolean; attemptsLeft: number } => { const now = Date.now(); const maxAttempts = 5; const windowMs = 15 * 60 * 1000; // 15 minutes const blockDurationMs = 60 * 60 * 1000; // 1 hour // Check if IP is blocked const blockedUntil = blockedIPs.get(ip); if (blockedUntil && now < blockedUntil) { return { allowed: false, attemptsLeft: 0 }; } // Clear expired blocks if (blockedUntil && now >= blockedUntil) { blockedIPs.delete(ip); } // Check current attempts const attempts = loginAttempts.get(ip); if (attempts && (now - attempts.lastAttempt) > windowMs) { loginAttempts.delete(ip); return { allowed: true, attemptsLeft: maxAttempts }; } const currentCount = attempts?.count || 0; if (currentCount >= maxAttempts) { blockedIPs.set(ip, now + blockDurationMs); return { allowed: false, attemptsLeft: 0 }; } return { allowed: true, attemptsLeft: maxAttempts - currentCount }; }; export const recordFailedAttempt = (ip: string): void => { const now = Date.now(); const attempts = loginAttempts.get(ip); if (attempts) { attempts.count += 1; attempts.lastAttempt = now; } else { loginAttempts.set(ip, { count: 1, lastAttempt: now }); } }; ``` ### **2. Input Validation** ```typescript // server/utils/validation.ts export const validateLoginInput = (username: string, password: string): string[] => { const errors: string[] = []; // Username validation if (!username || typeof username !== 'string') { errors.push('Username is required'); } else if (username.length < 2) { errors.push('Username must be at least 2 characters'); } else if (username.length > 100) { errors.push('Username is too long'); } else if (!/^[a-zA-Z0-9@._-]+$/.test(username)) { errors.push('Username contains invalid characters'); } // Password validation if (!password || typeof password !== 'string') { errors.push('Password is required'); } else if (password.length < 6) { errors.push('Password must be at least 6 characters'); } else if (password.length > 200) { errors.push('Password is too long'); } return errors; }; export const getClientIP = (event: any): string => { const headers = getHeaders(event); return ( headers['x-forwarded-for']?.split(',')[0]?.trim() || headers['x-real-ip'] || headers['x-client-ip'] || headers['cf-connecting-ip'] || 'unknown' ); }; ``` ### **3. Session Security** ```typescript // Secure cookie configuration setCookie(event, 'session-name', sessionId, { httpOnly: true, // Prevent XSS attacks secure: isProduction, // HTTPS only in production sameSite: 'lax', // CSRF protection maxAge: rememberMe ? 2592000 : 604800, // 30 days vs 7 days path: '/', // Available site-wide domain: process.env.NUXT_PUBLIC_DOMAIN ? `.${process.env.NUXT_PUBLIC_DOMAIN}` : undefined }); ``` ## ๐Ÿ”„ **Authentication Flow** ### **Complete Authentication Flow Diagram** ```mermaid sequenceDiagram participant U as User participant C as Client App participant S as Server API participant K as Keycloak participant D as Database U->>C: Navigate to /login C->>C: Guest middleware (allow if not authenticated) C->>U: Show login form U->>C: Submit credentials C->>S: POST /api/auth/direct-login S->>S: Validate input & rate limiting S->>K: POST /token (ROPC flow) K->>S: Return access token + refresh token S->>K: GET /userinfo (with access token) K->>S: Return user profile S->>S: Extract groups/roles, determine tier S->>D: Create encrypted session S->>C: Set secure cookie + return success C->>C: Navigate to /dashboard C->>S: GET /api/auth/session (via cookie) S->>S: Decrypt session, validate tokens S->>C: Return user data C->>C: Auth middleware passes C->>U: Show authenticated dashboard ``` ### **Session Lifecycle** 1. **Login**: User provides credentials โ†’ Server validates with Keycloak โ†’ Creates encrypted session 2. **Session Check**: Client calls `/api/auth/session` โ†’ Server validates session โ†’ Returns user data 3. **Token Refresh**: Server automatically refreshes tokens before expiry 4. **Logout**: Client calls `/api/auth/logout` โ†’ Server destroys session โ†’ Redirects to login ## ๐Ÿ› ๏ธ **Middleware System** ### **1. Auth Middleware (Protect Routes)** ```typescript // middleware/auth.ts export default defineNuxtRouteMiddleware(async (to) => { if (to.meta.auth === false) { return; // Skip auth for public pages } const { isAuthenticated, checkAuth, user } = useAuth(); // Ensure auth is checked if user isn't loaded if (!user.value) { await checkAuth(); } if (!isAuthenticated.value) { return navigateTo('/login'); } }); ``` ### **2. Guest Middleware (Redirect Authenticated Users)** ```typescript // middleware/guest.ts export default defineNuxtRouteMiddleware((to, from) => { const { user } = useAuth(); // If user is already authenticated, redirect to dashboard if (user.value) { return navigateTo('/dashboard'); } }); ``` ### **3. Role-Based Middleware** ```typescript // middleware/auth-admin.ts export default defineNuxtRouteMiddleware((to, from) => { const { isAuthenticated, isAdmin } = useAuth(); if (!isAuthenticated.value) { return navigateTo('/login'); } if (!isAdmin.value) { throw createError({ statusCode: 403, statusMessage: 'Access denied. Administrator privileges required.' }); } }); ``` ## ๐Ÿงช **Testing & Debugging** ### **1. Testing Checklist** **Backend Testing:** ```bash # Test direct login endpoint curl -X POST http://localhost:3000/api/auth/direct-login \ -H "Content-Type: application/json" \ -d '{"username":"test@example.com","password":"password123"}' # Test session endpoint with cookie curl -X GET http://localhost:3000/api/auth/session \ -H "Cookie: your-session-name=session-id-here" # Test logout endpoint curl -X POST http://localhost:3000/api/auth/logout \ -H "Cookie: your-session-name=session-id-here" ``` **Frontend Testing:** - [ ] **Login Form Validation**: Test empty fields, short passwords, invalid characters - [ ] **Login Flow**: Valid credentials โ†’ successful login โ†’ redirect to dashboard - [ ] **Error Handling**: Invalid credentials โ†’ proper error messages - [ ] **Session Management**: Page refresh โ†’ user stays logged in - [ ] **Mobile Compatibility**: Test on iOS Safari, Android Chrome - [ ] **Remember Me**: Long-term sessions work correctly **Security Testing:** - [ ] **Rate Limiting**: 5+ failed attempts โ†’ IP blocked for 1 hour - [ ] **CSRF Protection**: Direct API calls without cookies โ†’ rejected - [ ] **XSS Protection**: Malicious input โ†’ properly sanitized - [ ] **Session Security**: HttpOnly cookies โ†’ not accessible via JavaScript ### **2. Debug Tools** **Server-Side Debugging:** ```typescript // Add debug logging in server routes console.log('๐Ÿ”„ Login attempt:', { username: credentials.username, ip: getClientIP(event), timestamp: new Date().toISOString() }); console.log('โœ… Keycloak response:', { status: tokenResponse.status, hasTokens: !!tokens.access_token, userEmail: userInfo.email, groups: extractedGroups }); ``` **Client-Side Debugging:** ```typescript // Add debug logging in composables const checkAuth = async (): Promise => { console.log('๐Ÿ”„ Checking authentication...'); try { const response = await $fetch('/api/auth/session'); console.log('๐Ÿ” Session response:', { authenticated: response.authenticated, user: response.user?.email, tier: response.user?.tier }); // ... rest of method } catch (err) { console.error('โŒ Auth check failed:', err); // ... error handling } }; ``` **Browser DevTools Inspection:** - **Network Tab**: Monitor API calls to `/api/auth/*` - **Application Tab**: Check cookies are set correctly - **Console Tab**: Review authentication logs - **Sources Tab**: Set breakpoints in auth flow ## ๐Ÿšจ **Error Handling** ### **1. Server-Side Error Handling** ```typescript // server/api/auth/direct-login.post.ts export default defineEventHandler(async (event) => { try { // ... authentication logic } catch (error: any) { console.error('โŒ Login error:', { message: error.message, status: error.status, ip: getClientIP(event), timestamp: new Date().toISOString() }); // Map specific errors to user-friendly messages if (error.status === 401) { recordFailedAttempt(getClientIP(event)); throw createError({ statusCode: 401, statusMessage: 'Invalid username or password' }); } if (error.status === 429) { throw createError({ statusCode: 429, statusMessage: 'Too many login attempts. Please try again later.' }); } if (error.code === 'ECONNREFUSED') { throw createError({ statusCode: 503, statusMessage: 'Authentication service temporarily unavailable' }); } // Generic error for unexpected issues throw createError({ statusCode: 500, statusMessage: 'Login failed. Please try again.' }); } }); ``` ### **2. Client-Side Error Handling** ```typescript // composables/useAuth.ts const getErrorMessage = (err: any): string => { // Handle network errors if (!err.response) { return 'Network error. Please check your connection.'; } // Handle HTTP errors switch (err.status) { case 400: return err.data?.message || 'Invalid request. Please check your input.'; case 401: return 'Invalid username or password.'; case 403: return 'Access denied. Please contact your administrator.'; case 429: return 'Too many login attempts. Please try again later.'; case 502: case 503: return 'Service temporarily unavailable. Please try again.'; case 504: return 'Request timeout. Please try again.'; default: return err.data?.message || 'Login failed. Please try again.'; } }; ``` ### **3. User-Friendly Error Display** ```vue
Login Failed
{{ loginError }}
``` ## ๐Ÿš€ **Production Considerations** ### **1. Environment Configuration** ```bash # Production .env NODE_ENV=production # Use strong secrets in production NUXT_SESSION_SECRET=generate-a-very-strong-48-character-secret-key-here NUXT_ENCRYPTION_KEY=generate-strong-32-char-key-here # Keycloak production URLs NUXT_KEYCLOAK_ISSUER=https://auth.yourdomain.com/realms/your-realm NUXT_KEYCLOAK_CLIENT_ID=your-production-client NUXT_KEYCLOAK_CLIENT_SECRET=your-production-secret # Security settings NUXT_PUBLIC_DOMAIN=yourdomain.com ``` ### **2. Security Headers** ```typescript // nuxt.config.ts export default defineNuxtConfig({ nitro: { routeRules: { '/**': { headers: { // Security headers 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', // HTTPS enforcement 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', // Content Security Policy 'Content-Security-Policy': [ "default-src 'self'", "script-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com", "img-src 'self' data: https:", "connect-src 'self'" ].join('; ') } } } } }); ``` ### **3. Session Storage Options** **In-Memory (Development Only):** ```typescript // Simple Map - loses data on restart const sessions = new Map(); ``` **Redis (Recommended for Production):** ```typescript // server/utils/session-storage.ts import Redis from 'ioredis'; const redis = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: 0, retryDelayOnFailover: 100, maxRetriesPerRequest: 3 }); export const sessionStorage = { async set(key: string, value: string, ttl: number = 3600): Promise { await redis.setex(`session:${key}`, ttl, value); }, async get(key: string): Promise { return await redis.get(`session:${key}`); }, async delete(key: string): Promise { await redis.del(`session:${key}`); } }; ``` **Database (Alternative):** ```sql -- Session table schema CREATE TABLE user_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id VARCHAR(255) UNIQUE NOT NULL, user_id VARCHAR(255) NOT NULL, session_data TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_sessions_session_id ON user_sessions(session_id); CREATE INDEX idx_sessions_expires_at ON user_sessions(expires_at); ``` ### **4. Monitoring & Logging** ```typescript // server/utils/monitoring.ts export const logAuthEvent = (event: string, data: any) => { const logData = { timestamp: new Date().toISOString(), event, ip: data.ip || 'unknown', userAgent: data.userAgent || 'unknown', user: data.user || 'anonymous', success: data.success || false, error: data.error || null }; // Send to monitoring service (e.g., DataDog, New Relic, etc.) console.log(JSON.stringify(logData)); // In production, send to your logging service: // await sendToLoggingService(logData); }; // Usage in auth routes logAuthEvent('login_attempt', { ip: getClientIP(event), user: username, success: true }); ``` ### **5. Performance Optimization** ```typescript // Cache Keycloak public key for token validation let keycloakPublicKey: string | null = null; let keyLastFetched = 0; const KEY_CACHE_DURATION = 3600000; // 1 hour const getKeycloakPublicKey = async (): Promise => { const now = Date.now(); if (!keycloakPublicKey || (now - keyLastFetched) > KEY_CACHE_DURATION) { const config = useRuntimeConfig(); const response = await fetch(`${config.keycloak.issuer}/.well-known/openid_configuration`); const oidcConfig = await response.json(); const keysResponse = await fetch(oidcConfig.jwks_uri); const keys = await keysResponse.json(); // Extract public key from JWKS keycloakPublicKey = keys.keys[0].x5c[0]; keyLastFetched = now; } return keycloakPublicKey!; }; ``` ## ๐Ÿ”ง **Troubleshooting** ### **1. Common Issues** **Issue: "Invalid client credentials"** ```bash # Solution: Check Keycloak client configuration - Verify client ID matches environment variable - Ensure client secret is correct - Confirm client type is "confidential" - Check "Direct Access Grants" is enabled ``` **Issue: "CORS errors during login"** ```bash # Solution: Configure Keycloak CORS settings - In Keycloak Admin Console - Go to Client โ†’ Settings โ†’ Advanced Settings - Add your domain to "Web Origins" - Set "Valid Redirect URIs" properly ``` **Issue: "Session not persisting after login"** ```typescript // Solution: Check cookie configuration setCookie(event, 'session-name', sessionId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', // Important! sameSite: 'lax', domain: process.env.NODE_ENV === 'production' ? '.yourdomain.com' : undefined }); ``` **Issue: "Redirect loop after login"** ```typescript // Solution: Check middleware configuration // Ensure pages have correct middleware: - Login page: middleware: 'guest' - Dashboard pages: middleware: 'auth' - Index page: client-side navigation only ``` **Issue: "User groups/roles not being extracted"** ```typescript // Solution: Configure group mappers in Keycloak // 1. Go to Client Scopes โ†’ roles โ†’ Mappers // 2. Create mapper: "groups" // 3. Mapper Type: "Group Membership" // 4. Token Claim Name: "groups" // 5. Add to access token and ID token ``` ### **2. Debug Commands** ```bash # Check Keycloak connectivity curl -X GET "https://auth.yourdomain.com/realms/your-realm/.well-known/openid_configuration" # Test token endpoint directly curl -X POST "https://auth.yourdomain.com/realms/your-realm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password&client_id=your-client&client_secret=your-secret&username=test&password=test123" # Verify user info endpoint curl -X GET "https://auth.yourdomain.com/realms/your-realm/protocol/openid-connect/userinfo" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### **3. Common Configuration Mistakes** โŒ **Wrong Grant Type:** ```javascript // WRONG - Don't use authorization code flow for custom login standardFlowEnabled: true directAccessGrantsEnabled: false ``` โœ… **Correct Grant Type:** ```javascript // CORRECT - Use Resource Owner Password Credentials standardFlowEnabled: false directAccessGrantsEnabled: true ``` โŒ **Wrong Cookie Settings:** ```typescript // WRONG - Insecure cookie settings setCookie(event, 'session', sessionId, { httpOnly: false, // โŒ XSS vulnerable secure: false, // โŒ Not secure over HTTP sameSite: 'none' // โŒ CSRF vulnerable }); ``` โœ… **Correct Cookie Settings:** ```typescript // CORRECT - Secure cookie settings setCookie(event, 'session', sessionId, { httpOnly: true, // โœ… XSS protection secure: isProduction, // โœ… HTTPS in production sameSite: 'lax' // โœ… CSRF protection }); ``` ### **4. Performance Troubleshooting** **Slow Login Response:** - Check network latency to Keycloak server - Verify Keycloak server performance - Consider caching user info for short periods - Optimize session creation process **High Memory Usage:** - Implement session cleanup for expired sessions - Use external session storage (Redis) instead of memory - Set appropriate session TTL values **Database Connection Issues:** - Configure proper connection pooling - Set appropriate timeout values - Implement retry logic for transient failures ## ๐Ÿ“š **Additional Resources** ### **Keycloak Documentation** - [Resource Owner Password Credentials Flow](https://www.keycloak.org/docs/latest/securing_apps/#_resource_owner_password_credentials_flow) - [Client Configuration](https://www.keycloak.org/docs/latest/server_admin/#_clients) - [User Groups and Roles](https://www.keycloak.org/docs/latest/server_admin/#con-groups_server_administration_guide) ### **Security Best Practices** - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) - [OWASP Session Management](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) - [Cookie Security Guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security) ### **Nuxt 3 Specific** - [Nuxt 3 Authentication Patterns](https://nuxt.com/docs/guide/directory-structure/middleware#authentication) - [Server Routes](https://nuxt.com/docs/guide/directory-structure/server) - [Runtime Config](https://nuxt.com/docs/api/nuxt-config#runtimeconfig) --- ## ๐ŸŽ‰ **Conclusion** This comprehensive guide covers implementing a custom login page with Keycloak in a Nuxt 3 application. The solution provides: โœ… **Complete UI Control**: Custom branded login experience โœ… **Mobile Compatibility**: Works perfectly on all devices including iOS Safari โœ… **Enterprise Security**: Rate limiting, input validation, secure sessions โœ… **Production Ready**: Proper error handling, monitoring, and scalability โœ… **Maintainable**: Clean separation of concerns and well-documented code The implementation balances security, performance, and user experience while providing a solid foundation for enterprise authentication needs.