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
This commit is contained in:
Z User 2026-03-27 23:16:14 +00:00
parent 5ac38423c4
commit 4e0dd4c107

View File

@ -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);
}
}