Fix refresh token

This commit is contained in:
2025-10-22 14:03:21 +02:00
parent ffda3ad51c
commit 738f627f1f
6 changed files with 552 additions and 114 deletions

View File

@@ -7,7 +7,8 @@ import { createPinia } from 'pinia';
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { AuthService } from './service/AuthService.js';
import { TokenRefreshManager } from './service/TokenRefreshManager.js';
import '@/assets/styles.scss';
import '@/assets/tailwind.css';
@@ -20,16 +21,16 @@ import ConfirmationService from 'primevue/confirmationservice';
import ToastService from 'primevue/toastservice';
import { LoadingStore } from './stores/LoadingStore.js';
// Declare variables that will be used globally
let authInstance = null;
let tokenRefreshManager = null;
config({
editorConfig: {
renderDelay: 0,
zIndex: 200000000
}
});
});
var auth = createAuth({
plugins: {
http: axios,
@@ -40,23 +41,20 @@ var auth = createAuth({
auth: driverAuthBearer,
router: driverRouterVueRouter
},
options:{
options: {
notFoundRedirect: '/',
authRedirect: '/',
authRedirect: true, // niente redirect automatici
notFoundRedirect: false,
loginData: {url: '/api/auth/login', method: 'POST', redirect: '/home'},
logoutData: {url: '/api/auth/logout', redirect: '/auth/login'},
fetchData: {url: '/api/auth/fetch-user', method: 'GET', enabled: false},
refreshData: {url: '/api/auth/refresh-token', method: 'GET', enabled: true}
loginData: { url: '/api/auth/login', method: 'POST', redirect: '/home' },
logoutData: { url: '/api/auth/logout', redirect: '/auth/login' },
fetchData: { url: '/api/auth/fetch-user', method: 'GET', enabled: false },
refreshData: { url: '/api/auth/refresh-token', method: 'GET', enabled: true }
}
});
axios.defaults.baseURL = import.meta.env.VITE_BACKEND_URL;
//axios.defaults.baseURL ='http://localhost:8081';
//axios.defaults.baseURL = 'http://localhost:8081';
console.log(import.meta.env.VITE_BACKEND_URL);
@@ -95,7 +93,7 @@ const preset = definePreset(Nora, {
},
formField: {
hoverBorderColor: '{primary.color}',
borderColor: '{primary.color}',
borderColor: '{primary.color}'
}
},
dark: {
@@ -115,7 +113,7 @@ const preset = definePreset(Nora, {
},
formField: {
hoverBorderColor: '{primary.color}',
borderColor: '{primary.color}',
borderColor: '{primary.color}'
}
}
},
@@ -137,7 +135,7 @@ app.use(PrimeVue, {
prefix: 'p',
darkModeSelector: '.app-dark',
cssLayer: false
},
}
}
});
app.use(ToastService);
@@ -148,20 +146,108 @@ app.component('BlockViewer', BlockViewer);
app.mount('#app');
// Store auth instance for interceptor and token refresh manager after app is mounted
authInstance = app.config.globalProperties.$auth;
tokenRefreshManager = new TokenRefreshManager(authInstance);
// Start token refresh timer when user is authenticated
if (authInstance.check()) {
tokenRefreshManager.startRefreshTimer();
}
// Listen for successful login to start token refresh timer
window.addEventListener('auth-login-success', () => {
console.log('[Main] Login success event received, starting token refresh timer');
if (tokenRefreshManager) {
tokenRefreshManager.startRefreshTimer();
}
});
// Stop token refresh timer on logout
window.addEventListener('auth-logout', () => {
console.log('[Main] Logout event received, stopping token refresh timer');
if (tokenRefreshManager) {
tokenRefreshManager.stopRefreshTimer();
}
});
const loadingStore = LoadingStore();
axios.interceptors.request.use(function (config) {
axios.interceptors.request.use(
function (config) {
loadingStore.another_loading = true;
return config
}, function (error) {
return config;
},
function (error) {
return Promise.reject(error);
});
}
);
axios.interceptors.response.use(function (response) {
axios.interceptors.response.use(
function (response) {
loadingStore.another_loading = false;
return response;
},
async function (error) {
loadingStore.another_loading = false;
return response;
}, function (error) {
return Promise.reject(error);
});
// Don't handle auth-related URLs to prevent infinite loops
const isAuthUrl = error.config && (error.config.url.includes('/api/auth/') || error.config.url.includes('/msauth/'));
// Handle 403 errors by attempting token refresh (but not for auth URLs)
if (error.response && error.response.status === 403 && !isAuthUrl) {
console.log('[Interceptor] 403 error detected, attempting token refresh...');
// Check if we have an authenticated user and authInstance is available
if (authInstance && authInstance.check && authInstance.check()) {
try {
// Use our custom AuthService to refresh tokens (both MSAL and classic)
const refreshResult = await AuthService.refreshToken(authInstance);
if (refreshResult.success && refreshResult.token) {
console.log('[Interceptor] Token refresh successful, updating auth and retrying request...');
// Update the auth token
authInstance.token(null, refreshResult.token, false);
// Update the original request with new token
error.config.headers['Authorization'] = `Bearer ${refreshResult.token}`;
// Retry the original request
return axios.request(error.config);
} else {
console.error('[Interceptor] Token refresh failed:', refreshResult.error);
throw new Error(refreshResult.error);
}
} catch (refreshError) {
console.error('[Interceptor] Token refresh process failed:', refreshError);
// If refresh fails, logout and redirect to login
try {
// Stop token refresh timer
window.dispatchEvent(new CustomEvent('auth-logout'));
await AuthService.logout();
if (authInstance && authInstance.logout) {
authInstance.logout({
redirect: '/auth/login'
});
} else {
window.location.href = '/auth/login';
}
} catch (logoutError) {
console.error('[Interceptor] Logout failed:', logoutError);
// Force redirect to login
window.location.href = '/auth/login';
}
return Promise.reject(refreshError);
}
} else {
console.log('[Interceptor] No authenticated user for non-auth 403, ignoring...');
}
}
return Promise.reject(error);
}
);

View File

@@ -61,6 +61,11 @@ const router = createRouter({
name: 'app-browser',
component: () => import('@/views/pages/ApplicationBrowser.vue')
},
// {
// path: '/filesystem-browser',
// name: 'filesystem-browser',
// component: () => import('@/views/pages/FileSystemBrowser.vue')
// },
{
path: '/mdcanvas',
name: 'mdcanvas',
@@ -95,4 +100,17 @@ const router = createRouter({
]
});
// Navigation guard to handle logout when going to login page
router.beforeEach((to, from, next) => {
// Only trigger logout event when coming from an authenticated route to login
// and not on initial page load or when already on login/callback pages
if (to.name === 'login' && from.name && from.name !== 'login' && from.name !== 'test' && from.meta?.auth === true) {
// User is navigating to login page from an authenticated route
// This indicates a logout or session expiry
console.log('[Router] Navigating from authenticated route to login, triggering logout event');
window.dispatchEvent(new CustomEvent('auth-logout'));
}
next();
});
export default router;

215
src/service/AuthService.js Normal file
View File

@@ -0,0 +1,215 @@
import { msalInstance, msalrequest } from '@/views/pages/auth/MsalConfig';
import axios from 'axios';
export class AuthService {
/**
* Attempts to refresh tokens based on the authentication type
* @param {Object} authInstance - The vue-auth instance
* @returns {Promise<{success: boolean, token?: string, error?: string}>}
*/
static async refreshToken(authInstance = null) {
try {
console.log('[AuthService] Starting token refresh process...');
// Check if we have MSAL accounts first
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
// User logged in with MSAL - use MSAL refresh
console.log('[AuthService] Using MSAL token refresh...');
return await this.refreshMsalToken();
} else if (authInstance && authInstance.check()) {
// User logged in with classic authentication - use vue-auth refresh
console.log('[AuthService] Using classic authentication token refresh...');
return await this.refreshClassicToken(authInstance);
} else {
console.warn('[AuthService] No valid authentication method found');
return { success: false, error: 'No valid authentication method found' };
}
} catch (error) {
console.error('[AuthService] Token refresh failed:', error);
return { success: false, error: error.message };
}
}
/**
* Refreshes token using classic username/password authentication
* @param {Object} authInstance - The vue-auth instance
* @returns {Promise<{success: boolean, token?: string, error?: string}>}
*/
static async refreshClassicToken(authInstance) {
try {
console.log('[AuthService] Attempting classic token refresh...');
const refreshResponse = await authInstance.refresh();
console.log('[AuthService] Classic refresh response:', refreshResponse);
// Check multiple possible response structures
let token = null;
if (refreshResponse && refreshResponse.data) {
// Try different token field names
token = refreshResponse.data.token || refreshResponse.data.accessToken || refreshResponse.data.access_token;
// If still no token, check if the token is directly in data
if (!token && typeof refreshResponse.data === 'string') {
token = refreshResponse.data;
}
}
// Also check if token is in headers (as backend sets it there)
if (!token && refreshResponse && refreshResponse.headers) {
token = refreshResponse.headers.authorization || refreshResponse.headers.Authorization;
}
if (token) {
console.log('[AuthService] Classic token refresh successful');
return {
success: true,
token: token,
response: refreshResponse
};
} else {
console.error('[AuthService] No token found in classic refresh response:', refreshResponse);
return { success: false, error: 'No token in refresh response' };
}
} catch (error) {
console.error('[AuthService] Classic token refresh failed:', error);
return { success: false, error: 'Classic token refresh failed' };
}
}
/**
* Attempts to refresh the MSAL token and exchange it for a new backend token
* @returns {Promise<{success: boolean, token?: string, error?: string}>}
*/
static async refreshMsalToken() {
try {
console.log('[AuthService] Starting MSAL token refresh process...');
// Get all accounts from MSAL
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
console.warn('[AuthService] No MSAL accounts found for MSAL refresh');
return { success: false, error: 'No MSAL accounts found' };
}
const account = accounts[0];
console.log('[AuthService] Using account:', account.username);
// Try to acquire token silently
let tokenResponse;
try {
tokenResponse = await msalInstance.acquireTokenSilent({
scopes: msalrequest.scopes,
account: account
});
console.log('[AuthService] MSAL token acquired silently');
} catch (silentError) {
console.warn('[AuthService] Silent token acquisition failed, trying interactive...');
// If silent fails, try interactive
try {
tokenResponse = await msalInstance.acquireTokenPopup({
scopes: msalrequest.scopes,
account: account
});
console.log('[AuthService] MSAL token acquired interactively');
} catch (interactiveError) {
console.error('[AuthService] Interactive token acquisition failed:', interactiveError);
return { success: false, error: 'Token acquisition failed' };
}
}
if (!tokenResponse || !tokenResponse.accessToken) {
console.error('[AuthService] No access token received');
return { success: false, error: 'No access token received' };
}
// Exchange the MSAL token for a backend token
try {
const exchangeResponse = await axios.post(
'/msauth/exchange',
{},
{
headers: { Authorization: `Bearer ${tokenResponse.accessToken}` }
}
);
if (exchangeResponse.data && exchangeResponse.data.token) {
console.log('[AuthService] MSAL token refresh successful');
return {
success: true,
token: exchangeResponse.data.token,
response: exchangeResponse
};
} else {
console.error('[AuthService] No token in MSAL exchange response');
return { success: false, error: 'No token in exchange response' };
}
} catch (exchangeError) {
console.error('[AuthService] MSAL token exchange failed:', exchangeError);
return { success: false, error: 'Token exchange failed' };
}
} catch (error) {
console.error('[AuthService] MSAL token refresh failed:', error);
return { success: false, error: error.message };
}
}
/**
* Checks if the current MSAL session is still valid
* @returns {Promise<boolean>}
*/
static async isSessionValid() {
try {
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
return false;
}
// Try to acquire token silently to check if session is valid
try {
await msalInstance.acquireTokenSilent({
scopes: msalrequest.scopes,
account: accounts[0]
});
return true;
} catch (error) {
console.warn('[AuthService] Session validation failed:', error);
return false;
}
} catch (error) {
console.error('[AuthService] Session validation error:', error);
return false;
}
}
/**
* Logs out from MSAL
* @returns {Promise<void>}
*/
static async logout() {
try {
await msalInstance.initialize();
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
const logoutRequest = {
account: accounts[0],
mainWindowRedirectUri: window.location.origin + '/auth/login'
};
await msalInstance.logoutPopup(logoutRequest);
console.log('[AuthService] MSAL logout completed');
}
} catch (error) {
console.error('[AuthService] Logout error:', error);
}
}
}

View File

@@ -0,0 +1,106 @@
import { AuthService } from './AuthService.js';
export class TokenRefreshManager {
constructor(authInstance) {
this.authInstance = authInstance;
this.refreshInterval = null;
this.isRefreshing = false;
// Refresh every 50 minutes (tokens expire after 1 hour)
this.REFRESH_INTERVAL_MS = 50 * 60 * 1000;
}
/**
* Starts the automatic token refresh timer
*/
startRefreshTimer() {
console.log('[TokenRefreshManager] Starting automatic token refresh timer...');
// Clear any existing timer
this.stopRefreshTimer();
this.refreshInterval = setInterval(async () => {
await this.performRefresh();
}, this.REFRESH_INTERVAL_MS);
console.log(`[TokenRefreshManager] Timer set to refresh every ${this.REFRESH_INTERVAL_MS / 60000} minutes`);
}
/**
* Stops the automatic token refresh timer
*/
stopRefreshTimer() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
console.log('[TokenRefreshManager] Token refresh timer stopped');
}
}
/**
* Performs a token refresh
*/
async performRefresh() {
if (this.isRefreshing) {
console.log('[TokenRefreshManager] Refresh already in progress, skipping...');
return;
}
// Only refresh if user is authenticated
if (!this.authInstance || !this.authInstance.check()) {
console.log('[TokenRefreshManager] User not authenticated, stopping refresh timer');
this.stopRefreshTimer();
return;
}
this.isRefreshing = true;
try {
console.log('[TokenRefreshManager] Performing scheduled token refresh...');
const refreshResult = await AuthService.refreshToken(this.authInstance);
if (refreshResult.success && refreshResult.token) {
// Update the auth token silently
this.authInstance.token(null, refreshResult.token, false);
console.log('[TokenRefreshManager] Token refreshed successfully');
} else {
console.error('[TokenRefreshManager] Scheduled refresh failed:', refreshResult.error);
// Stop the timer and logout on failure
this.stopRefreshTimer();
try {
// Dispatch logout event to stop other timers
window.dispatchEvent(new CustomEvent('auth-logout'));
await AuthService.logout();
this.authInstance.logout({
redirect: '/auth/login'
});
} catch (logoutError) {
console.error('[TokenRefreshManager] Logout failed:', logoutError);
window.location.href = '/auth/login';
}
}
} catch (error) {
console.error('[TokenRefreshManager] Refresh error:', error);
this.stopRefreshTimer();
} finally {
this.isRefreshing = false;
}
}
/**
* Manually triggers a token refresh
* @returns {Promise<boolean>} Success status
*/
async manualRefresh() {
if (this.isRefreshing) {
console.log('[TokenRefreshManager] Manual refresh requested but already in progress');
return false;
}
await this.performRefresh();
return true;
}
}

View File

@@ -18,7 +18,7 @@ onMounted(async () => {
console.log('[Callback] After initialize on callback');
} catch (e) {
console.error('[Callback] Error during MSAL initialization:', e);
message.value = "Error during MSAL initialization.";
message.value = 'Error during MSAL initialization.';
visible.value = true;
return;
}
@@ -45,7 +45,7 @@ onMounted(async () => {
}
// Wait 2 second to avoid race condition
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
message.value = 'Logging in to the application...';
// Token exchange function with retry
@@ -63,7 +63,7 @@ onMounted(async () => {
} catch (err) {
if (!retry) {
console.warn('[Callback] First attempt failed, waiting 1500ms and retrying...');
await new Promise(resolve => setTimeout(resolve, 1500));
await new Promise((resolve) => setTimeout(resolve, 1500));
return tryTokenExchange(true);
} else {
throw err;
@@ -85,6 +85,9 @@ onMounted(async () => {
const userResponse = await auth.fetch();
console.log('[Callback] User fetch response:', userResponse);
// Start token refresh timer after successful authentication
window.dispatchEvent(new CustomEvent('auth-login-success'));
const userData = userResponse.data?.data;
console.log('[Callback] userResponse.data.data:', userData);
@@ -116,7 +119,6 @@ onMounted(async () => {
visible.value = true;
}
});
</script>
<template>

View File

@@ -45,7 +45,7 @@ const tryTokenExchange = async (accessToken, retry = false) => {
} catch (err) {
if (!retry) {
console.warn('[loginAD] First attempt failed, waiting 1500ms and retrying...');
await new Promise(r => setTimeout(r, 1500));
await new Promise((r) => setTimeout(r, 1500));
return tryTokenExchange(accessToken, true);
} else {
throw err;
@@ -63,7 +63,7 @@ const loginAD = async () => {
console.log('[loginAD] Token MSAL ottenuto:', token);
// Inserisci una pausa per evitare race condition
await new Promise(r => setTimeout(r, 2000));
await new Promise((r) => setTimeout(r, 2000));
let exchangeResponse;
try {
@@ -76,6 +76,9 @@ const loginAD = async () => {
const resp = await auth.fetch();
console.log('[loginAD] User fetch response:', resp.data.data);
// Start token refresh timer after successful authentication
window.dispatchEvent(new CustomEvent('auth-login-success'));
const userData = resp.data.data;
if (!userData.selectedProject) {
console.log('[loginAD] No project selected → projects-list');
@@ -96,7 +99,7 @@ const loginAD = async () => {
}
} catch (e) {
console.error('[loginAD] Error:', e);
error.value = "Error while login AD. Contact the administrator.";
error.value = 'Error while login AD. Contact the administrator.';
visible.value = true;
}
};
@@ -115,6 +118,10 @@ const loginMsal = async () => {
const logoutAD = async () => {
console.log('[logoutAD] Logout AD...');
// Stop token refresh timer
window.dispatchEvent(new CustomEvent('auth-logout'));
const logoutRequest = {
account: msaccount.value,
mainWindowRedirectUri: window.location.href
@@ -142,6 +149,10 @@ const login_old = async () => {
})
.then((response) => {
console.log('[login_old] Login response:', response.data.data);
// Start token refresh timer after successful authentication
window.dispatchEvent(new CustomEvent('auth-login-success'));
if (!response.data.data.selectedProject) {
console.log('[login_old] No project selected, redirect to projects-list');
router.push({ name: 'projects-list' });
@@ -169,9 +180,9 @@ const login_old = async () => {
<div class="bg-surface-50 dark:bg-surface-950 flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden">
<div class="flex flex-col items-center justify-center">
<div style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
<div class="w-full bg-surface-0 dark:bg-surface-900 py-20 px-8 sm:px-20" style="border-radius: 53px">
<div class="logo-container mb-8">
<svg width="85" height="63" viewBox="0 0 85 63" fill="none" xmlns="http://www.w3.org/2000/svg">
<div class="w-full bg-surface-0 dark:bg-surface-900 py-12 px-6 sm:px-12" style="border-radius: 53px">
<div class="logo-container mb-6">
<svg width="70" height="45" viewBox="0 0 85 63" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -189,36 +200,36 @@ const login_old = async () => {
</svg>
</div>
<div class="text-center mb-12">
<h1 class="text-3xl font-semibold">Welcome to</h1>
<h1 class="text-4xl font-bold text-primary">WizardAI - WORKFLOW</h1>
<div class="text-center mb-8">
<h1 class="text-2xl font-semibold">Welcome to</h1>
<h1 class="text-3xl font-bold text-primary">WizardAI - WORKFLOW</h1>
</div>
<!-- Username & Password Section -->
<div class="mb-10 w-full max-w-xl">
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-l font-medium mb-2">Username</label>
<InputText id="email1" type="text" placeholder="Username" class="w-full mb-6" style="padding: 1rem" v-model="username" />
<div class="mb-6 w-full max-w-md">
<label for="email1" class="block text-surface-900 dark:text-surface-0 text-sm font-medium mb-2">Username</label>
<InputText id="email1" type="text" placeholder="Username" class="w-full mb-4" style="padding: 0.75rem" v-model="username" />
<label for="password1" class="block text-surface-900 dark:text-surface-0 font-medium text-l mb-2">Password</label>
<Password id="password1" v-model="password" placeholder="Password" :toggleMask="true" class="w-full mb-6" inputClass="w-full" :inputStyle="{ padding: '1rem' }" />
<label for="password1" class="block text-surface-900 dark:text-surface-0 font-medium text-sm mb-2">Password</label>
<Password id="password1" v-model="password" placeholder="Password" :toggleMask="true" class="w-full mb-4" inputClass="w-full" :inputStyle="{ padding: '0.75rem' }" />
<Button @click="login_old" label="Sign In with Username and Password" class="w-full text-xl mb-4" />
<Button @click="login_old" label="Sign In with Username and Password" class="w-full text-sm mb-3" />
</div>
<!-- Divider -->
<div class="my-6 w-full max-w-xl flex items-center justify-center">
<div class="my-4 w-full max-w-md flex items-center justify-center">
<hr class="flex-grow border-t border-gray-300 dark:border-gray-700" />
<span class="mx-4 text-gray-500 dark:text-gray-400">Sign in with Microsoft Azure AD</span>
<span class="mx-3 text-sm text-gray-500 dark:text-gray-400">Sign in with Microsoft Azure AD</span>
<hr class="flex-grow border-t border-gray-300 dark:border-gray-700" />
</div>
<!-- Azure AD Section -->
<div class="w-full max-w-xl">
<Button @click="loginMsal" v-if="msaccount == null" label="Sign In with Microsoft AD" class="w-full text-l mb-4" />
<div class="w-full max-w-md">
<Button @click="loginMsal" v-if="msaccount == null" label="Sign In with Microsoft AD" class="w-full text-sm mb-3" />
<Button @click="loginAD" v-if="msaccount" :label="'Login ' + msaccount.username" class="w-full text-l mb-4" />
<Button @click="loginAD" v-if="msaccount" :label="'Login ' + msaccount.username" class="w-full text-sm mb-3" />
<Button @click="logoutAD" v-if="msaccount" label="Logout AD" severity="warn" class="w-full text-l mb-4" />
<Button @click="logoutAD" v-if="msaccount" label="Logout AD" severity="warn" class="w-full text-sm mb-3" />
</div>
<!-- Error Message -->
@@ -244,8 +255,8 @@ const login_old = async () => {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
margin-top: -40px;
margin-bottom: 15px;
margin-top: -30px;
}
.error-message {