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
|
* Moxiegen Authentication Module
|
||||||
* Handles Auth0 authentication, login/logout, and token management
|
* Handles Auth0 authentication with backend token exchange
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class MoxieAuth {
|
class MoxieAuth {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.auth0Client = null;
|
|
||||||
this.isAuthenticated = false;
|
this.isAuthenticated = false;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.token = null;
|
this.token = null;
|
||||||
|
this.expiresAt = null;
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Auth0 client
|
* Initialize - check for existing session or handle callback
|
||||||
*/
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Dynamically load Auth0 SPA SDK if not already loaded
|
// Check for tokens in URL (coming back from token exchange)
|
||||||
if (typeof auth0 === 'undefined') {
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
await this.loadScript('https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js');
|
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
|
if (error) {
|
||||||
this.auth0Client = await auth0.createAuth0Client({
|
console.error('Auth error:', error, urlParams.get('error_description'));
|
||||||
domain: CONFIG.auth0.domain,
|
this.clearTokens();
|
||||||
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
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
throw new Error(urlParams.get('error_description') || error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication status
|
// If we have a code but no token, exchange it via backend
|
||||||
this.isAuthenticated = await this.auth0Client.isAuthenticated();
|
if (code && !accessToken) {
|
||||||
|
console.log('Exchanging authorization code for tokens...');
|
||||||
|
await this.exchangeCode(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isAuthenticated) {
|
if (accessToken) {
|
||||||
this.user = await this.auth0Client.getUser();
|
// Store tokens from URL
|
||||||
this.token = await this.auth0Client.getTokenSilently();
|
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;
|
this.isInitialized = true;
|
||||||
console.log('Auth0 initialized. Authenticated:', this.isAuthenticated);
|
console.log('Auth initialized. Authenticated:', this.isAuthenticated);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize Auth0:', error);
|
console.error('Failed to initialize auth:', error);
|
||||||
|
this.isInitialized = true;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load external script
|
* Exchange authorization code for tokens via backend
|
||||||
*/
|
*/
|
||||||
loadScript(src) {
|
async exchangeCode(code) {
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
const script = document.createElement('script');
|
const response = await fetch('/api/auth/token', {
|
||||||
script.src = src;
|
method: 'POST',
|
||||||
script.onload = resolve;
|
headers: { 'Content-Type': 'application/json' },
|
||||||
script.onerror = reject;
|
body: JSON.stringify({
|
||||||
document.head.appendChild(script);
|
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
|
* Login - redirect to Auth0
|
||||||
*/
|
*/
|
||||||
async login(returnTo = null) {
|
async login(returnTo = null) {
|
||||||
if (!this.auth0Client) {
|
const redirectUri = returnTo || window.location.origin + '/dashboard.html';
|
||||||
await this.init();
|
|
||||||
}
|
// 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({
|
window.location.href = authUrl;
|
||||||
authorizationParams: {
|
|
||||||
audience: CONFIG.auth0.audience,
|
|
||||||
redirect_uri: returnTo || CONFIG.auth0.redirectUri
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout
|
* Logout
|
||||||
*/
|
*/
|
||||||
async logout() {
|
async logout() {
|
||||||
if (!this.auth0Client) {
|
this.clearTokens();
|
||||||
await this.init();
|
|
||||||
}
|
const returnTo = encodeURIComponent(window.location.origin);
|
||||||
|
const logoutUrl = `https://${CONFIG.auth0.domain}/v2/logout?client_id=${CONFIG.auth0.clientId}&returnTo=${returnTo}`;
|
||||||
// Clear local state
|
|
||||||
this.isAuthenticated = false;
|
window.location.href = logoutUrl;
|
||||||
this.user = null;
|
|
||||||
this.token = null;
|
|
||||||
|
|
||||||
// Logout from Auth0
|
|
||||||
await this.auth0Client.logout({
|
|
||||||
logoutParams: {
|
|
||||||
returnTo: CONFIG.auth0.logoutUri
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {
|
async getToken() {
|
||||||
if (!this.auth0Client) {
|
if (!this.isInitialized) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isAuthenticated) {
|
// Check if token is expired
|
||||||
|
if (this.expiresAt && Date.now() >= this.expiresAt) {
|
||||||
|
this.clearTokens();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return this.token;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current user
|
* Get current user
|
||||||
*/
|
*/
|
||||||
async getUser() {
|
async getUser() {
|
||||||
if (!this.auth0Client) {
|
if (!this.isInitialized) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
return this.user;
|
return this.user;
|
||||||
@ -148,14 +232,9 @@ class MoxieAuth {
|
|||||||
* Check if user is authenticated
|
* Check if user is authenticated
|
||||||
*/
|
*/
|
||||||
async checkAuth() {
|
async checkAuth() {
|
||||||
if (!this.auth0Client) {
|
if (!this.isInitialized) {
|
||||||
await this.init();
|
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;
|
return this.isAuthenticated;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,8 +243,10 @@ class MoxieAuth {
|
|||||||
*/
|
*/
|
||||||
hasRole(role) {
|
hasRole(role) {
|
||||||
if (!this.user) return false;
|
if (!this.user) return false;
|
||||||
const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] || [];
|
const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] ||
|
||||||
return roles.includes(role);
|
this.user.roles ||
|
||||||
|
[];
|
||||||
|
return Array.isArray(roles) && roles.includes(role);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user