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:
parent
5ac38423c4
commit
4e0dd4c107
251
js/auth.js
251
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user