/** * Moxiegen Authentication Module * Handles Auth0 authentication with backend token exchange */ class MoxieAuth { constructor() { this.isAuthenticated = false; this.user = null; this.token = null; this.expiresAt = null; this.isInitialized = false; } /** * Initialize - check for existing session or handle callback */ async init() { if (this.isInitialized) return; try { // Check for tokens in URL (coming back from token exchange) const urlParams = new URLSearchParams(window.location.search); const accessToken = urlParams.get('access_token'); const idToken = urlParams.get('id_token'); const expiresIn = urlParams.get('expires_in'); const code = urlParams.get('code'); const error = urlParams.get('error'); if (error) { console.error('Auth error:', error, urlParams.get('error_description')); this.clearTokens(); window.history.replaceState({}, document.title, window.location.pathname); throw new Error(urlParams.get('error_description') || error); } // If we have a code but no token, exchange it via backend if (code && !accessToken) { console.log('Exchanging authorization code for tokens...'); await this.exchangeCode(code); return; } if (accessToken) { // Store tokens from URL this.token = accessToken; this.expiresAt = Date.now() + (parseInt(expiresIn) || 3600) * 1000; if (idToken) { this.user = this.decodeJwt(idToken); localStorage.setItem('id_token', idToken); } localStorage.setItem('access_token', accessToken); localStorage.setItem('expires_at', this.expiresAt.toString()); // Clean up URL window.history.replaceState({}, document.title, window.location.pathname); this.isAuthenticated = true; this.isInitialized = true; return; } // Check for stored tokens const storedToken = localStorage.getItem('access_token'); const storedExpiresAt = localStorage.getItem('expires_at'); const storedIdToken = localStorage.getItem('id_token'); if (storedToken && storedExpiresAt) { if (Date.now() < parseInt(storedExpiresAt)) { this.token = storedToken; this.expiresAt = parseInt(storedExpiresAt); if (storedIdToken) { this.user = this.decodeJwt(storedIdToken); } this.isAuthenticated = true; } else { // Token expired - clear it this.clearTokens(); } } this.isInitialized = true; console.log('Auth initialized. Authenticated:', this.isAuthenticated); } catch (error) { console.error('Failed to initialize auth:', error); this.isInitialized = true; throw error; } } /** * Exchange authorization code for tokens via backend */ async exchangeCode(code) { try { const response = await fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, redirect_uri: window.location.origin + '/dashboard.html' }) }); const data = await response.json(); if (!data.success) { throw new Error(data.message || 'Token exchange failed'); } const { access_token, id_token, expires_in } = data.data; // Store tokens this.token = access_token; this.expiresAt = Date.now() + (expires_in || 3600) * 1000; if (id_token) { this.user = this.decodeJwt(id_token); localStorage.setItem('id_token', id_token); } localStorage.setItem('access_token', access_token); localStorage.setItem('expires_at', this.expiresAt.toString()); // Clean up URL window.history.replaceState({}, document.title, window.location.pathname); this.isAuthenticated = true; this.isInitialized = true; console.log('Token exchange successful'); } catch (error) { console.error('Token exchange error:', error); // Redirect to login on failure this.clearTokens(); window.history.replaceState({}, document.title, window.location.pathname); throw error; } } /** * Decode JWT (just the payload, no verification) */ decodeJwt(token) { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); return JSON.parse(jsonPayload); } catch (e) { console.error('Failed to decode JWT:', e); return null; } } /** * Login - redirect to Auth0 */ async login(returnTo = null) { const redirectUri = returnTo || window.location.origin + '/dashboard.html'; // Build Auth0 authorize URL let authUrl = `https://${CONFIG.auth0.domain}/authorize?` + `response_type=code&` + `client_id=${CONFIG.auth0.clientId}&` + `redirect_uri=${encodeURIComponent(redirectUri)}&` + `scope=openid%20profile%20email`; // Only add audience if it's defined and not empty if (CONFIG.auth0.audience && CONFIG.auth0.audience.trim() !== '') { authUrl += `&audience=${encodeURIComponent(CONFIG.auth0.audience)}`; } window.location.href = authUrl; } /** * Logout */ async logout() { this.clearTokens(); const returnTo = encodeURIComponent(window.location.origin); const logoutUrl = `https://${CONFIG.auth0.domain}/v2/logout?client_id=${CONFIG.auth0.clientId}&returnTo=${returnTo}`; window.location.href = logoutUrl; } /** * Clear stored tokens */ clearTokens() { localStorage.removeItem('access_token'); localStorage.removeItem('id_token'); localStorage.removeItem('expires_at'); this.token = null; this.user = null; this.isAuthenticated = false; this.expiresAt = null; } /** * Get access token */ async getToken() { if (!this.isInitialized) { await this.init(); } // Check if token is expired if (this.expiresAt && Date.now() >= this.expiresAt) { this.clearTokens(); return null; } return this.token; } /** * Get current user */ async getUser() { if (!this.isInitialized) { await this.init(); } return this.user; } /** * Check if user is authenticated */ async checkAuth() { if (!this.isInitialized) { await this.init(); } return this.isAuthenticated; } /** * Check if user has specific role */ hasRole(role) { if (!this.user) return false; const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] || this.user.roles || []; return Array.isArray(roles) && roles.includes(role); } } // Create singleton instance const moxieAuth = new MoxieAuth(); // Export for use in other modules window.moxieAuth = moxieAuth;