From 4e0dd4c1076fc68f488076f417c1050e0b244eff Mon Sep 17 00:00:00 2001 From: Z User Date: Fri, 27 Mar 2026 23:16:14 +0000 Subject: [PATCH] Simplify auth flow for Regular Web Application - Remove Auth0 SPA SDK dependency - Use direct redirects to Auth0 - Exchange code via backend POST /api/auth/token - Store tokens in localStorage - Handle callback with code parameter --- js/auth.js | 251 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 85 deletions(-) diff --git a/js/auth.js b/js/auth.js index ce61de7..f8acf95 100644 --- a/js/auth.js +++ b/js/auth.js @@ -1,144 +1,228 @@ /** * Moxiegen Authentication Module - * Handles Auth0 authentication, login/logout, and token management + * Handles Auth0 authentication with backend token exchange */ class MoxieAuth { constructor() { - this.auth0Client = null; this.isAuthenticated = false; this.user = null; this.token = null; + this.expiresAt = null; this.isInitialized = false; } /** - * Initialize Auth0 client + * Initialize - check for existing session or handle callback */ async init() { if (this.isInitialized) return; try { - // Dynamically load Auth0 SPA SDK if not already loaded - if (typeof auth0 === 'undefined') { - await this.loadScript('https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js'); - } + // 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'); - // Create Auth0 client - this.auth0Client = await auth0.createAuth0Client({ - domain: CONFIG.auth0.domain, - clientId: CONFIG.auth0.clientId, - authorizationParams: { - audience: CONFIG.auth0.audience, - redirect_uri: CONFIG.auth0.redirectUri - }, - cacheLocation: 'localstorage', - useRefreshTokens: true - }); - - // Check if coming back from Auth0 redirect - const query = window.location.search; - if (query.includes('code=') && query.includes('state=')) { - await this.auth0Client.handleRedirectCallback(); - // Remove query params from URL + 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); } - // Check authentication status - this.isAuthenticated = await this.auth0Client.isAuthenticated(); + // 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 (this.isAuthenticated) { - this.user = await this.auth0Client.getUser(); - this.token = await this.auth0Client.getTokenSilently(); + 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('Auth0 initialized. Authenticated:', this.isAuthenticated); + console.log('Auth initialized. Authenticated:', this.isAuthenticated); } catch (error) { - console.error('Failed to initialize Auth0:', error); + console.error('Failed to initialize auth:', error); + this.isInitialized = true; throw error; } } /** - * Load external script + * Exchange authorization code for tokens via backend */ - loadScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = src; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); + 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) { - if (!this.auth0Client) { - await this.init(); - } + const redirectUri = returnTo || window.location.origin + '/dashboard.html'; + + // Build Auth0 authorize URL + const authUrl = `https://${CONFIG.auth0.domain}/authorize?` + + `response_type=code&` + + `client_id=${CONFIG.auth0.clientId}&` + + `redirect_uri=${encodeURIComponent(redirectUri)}&` + + `scope=openid%20profile%20email&` + + `audience=${encodeURIComponent(CONFIG.auth0.audience)}`; - await this.auth0Client.loginWithRedirect({ - authorizationParams: { - audience: CONFIG.auth0.audience, - redirect_uri: returnTo || CONFIG.auth0.redirectUri - } - }); + window.location.href = authUrl; } /** * Logout */ async logout() { - if (!this.auth0Client) { - await this.init(); - } - - // Clear local state - this.isAuthenticated = false; - this.user = null; - this.token = null; - - // Logout from Auth0 - await this.auth0Client.logout({ - logoutParams: { - returnTo: CONFIG.auth0.logoutUri - } - }); + 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; } /** - * Get access token (silently refreshes if needed) + * 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.auth0Client) { + if (!this.isInitialized) { await this.init(); } - if (!this.isAuthenticated) { + // Check if token is expired + if (this.expiresAt && Date.now() >= this.expiresAt) { + this.clearTokens(); return null; } - try { - this.token = await this.auth0Client.getTokenSilently(); - return this.token; - } catch (error) { - console.error('Failed to get token:', error); - // Token might be expired, try to re-authenticate - this.isAuthenticated = false; - return null; - } + return this.token; } /** * Get current user */ async getUser() { - if (!this.auth0Client) { + if (!this.isInitialized) { await this.init(); } return this.user; @@ -148,14 +232,9 @@ class MoxieAuth { * Check if user is authenticated */ async checkAuth() { - if (!this.auth0Client) { + if (!this.isInitialized) { await this.init(); } - this.isAuthenticated = await this.auth0Client.isAuthenticated(); - if (this.isAuthenticated) { - this.user = await this.auth0Client.getUser(); - this.token = await this.auth0Client.getTokenSilently(); - } return this.isAuthenticated; } @@ -164,8 +243,10 @@ class MoxieAuth { */ hasRole(role) { if (!this.user) return false; - const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] || []; - return roles.includes(role); + const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] || + this.user.roles || + []; + return Array.isArray(roles) && roles.includes(role); } }