Questa guida spiega come gestire correttamente il rinnovo dei token di accesso quando scadono.
Il sistema utilizza un meccanismo a doppio token:
Quando un access token scade, puoi usare il refresh token per ottenere un nuovo access token senza dover rifare login.
POST /api/v1/auth/refresh
Body della richiesta:
{
"token": "v4.public.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ..."
}
Nota: Il token deve essere un refresh token, non un access token.
Risposta:
{
"status": "success",
"data": {
"accessToken": "v4.public.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ...",
"expiresIn": 3600
}
}
Ecco un esempio completo di gestione automatica del refresh:
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
private refreshPromise: Promise<string> | null = null;
constructor() {
// Carica token da storage
this.loadTokens();
}
private loadTokens() {
this.accessToken = localStorage.getItem("accessToken");
this.refreshToken = localStorage.getItem("refreshToken");
}
private saveTokens(accessToken: string, refreshToken?: string) {
this.accessToken = accessToken;
localStorage.setItem("accessToken", accessToken);
if (refreshToken) {
this.refreshToken = refreshToken;
localStorage.setItem("refreshToken", refreshToken);
}
}
async refreshAccessToken(): Promise<string> {
// Evita chiamate multiple simultanee
if (this.refreshPromise) {
return this.refreshPromise;
}
if (!this.refreshToken) {
throw new Error("No refresh token available");
}
this.refreshPromise = (async () => {
try {
const response = await fetch("/api/v1/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: this.refreshToken }),
});
const data = await response.json();
if (data.status === "error") {
// Refresh token scaduto o non valido
this.clearTokens();
throw new Error("Refresh token expired");
}
const newAccessToken = data.data.accessToken;
this.saveTokens(newAccessToken);
return newAccessToken;
} finally {
this.refreshPromise = null;
}
})();
return this.refreshPromise;
}
async getAccessToken(): Promise<string> {
if (!this.accessToken) {
throw new Error("Not authenticated");
}
// Verifica se il token è scaduto (decodifica payload se necessario)
// Per semplicità, assumiamo che il token sia valido se presente
// In produzione, dovresti verificare la scadenza
return this.accessToken;
}
async makeRequest(url: string, options: RequestInit = {}): Promise<Response> {
let accessToken = await this.getAccessToken();
const makeRequestWithToken = (token: string) => {
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
};
let response = await makeRequestWithToken(accessToken);
// Se token scaduto, prova a rinnovare
if (response.status === 401) {
try {
accessToken = await this.refreshAccessToken();
response = await makeRequestWithToken(accessToken);
} catch (error) {
// Refresh fallito, redirect a login
this.clearTokens();
window.location.href = "/login";
throw error;
}
}
return response;
}
private clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
}
}
Puoi verificare se un token è scaduto decodificando il payload:
function isTokenExpired(token: string): boolean {
try {
// PASETO tokens hanno il formato: version.purpose.payload
const parts = token.split(".");
if (parts.length !== 3) return true;
// Decodifica base64 del payload
const payload = JSON.parse(atob(parts[2]));
// Verifica scadenza
if (payload.exp && payload.exp < Date.now() / 1000) {
return true;
}
return false;
} catch (error) {
return true;
}
}
Puoi rinnovare il token prima che scada:
class ProactiveTokenManager extends TokenManager {
private refreshTimer: NodeJS.Timeout | null = null;
startProactiveRefresh() {
// Rinnova il token quando mancano 5 minuti alla scadenza
const refreshInterval = (3600 - 300) * 1000; // 55 minuti
this.refreshTimer = setInterval(async () => {
try {
await this.refreshAccessToken();
console.log("Token refreshed proactively");
} catch (error) {
console.error("Failed to refresh token:", error);
this.stopProactiveRefresh();
}
}, refreshInterval);
}
stopProactiveRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
}
}
Quando il refresh token è scaduto, riceverai:
{
"status": "error",
"error": {
"code": "REFRESH_TOKEN_INVALID",
"message": "Refresh token non valido o scaduto"
}
}
In questo caso, l'utente deve rifare login:
if (error.code === "REFRESH_TOKEN_INVALID") {
// Rimuovi token salvati
tokenManager.clearTokens();
// Redirect a login
window.location.href = "/login";
}
Se il refresh token non è valido o è stato revocato:
{
"status": "error",
"error": {
"code": "INVALID_TOKEN",
"message": "Token non valido"
}
}
// Interceptor per axios
import axios from "axios";
const tokenManager = new TokenManager();
axios.interceptors.request.use(async (config) => {
const token = await tokenManager.getAccessToken();
config.headers.Authorization = `Bearer ${token}`;
return config;
});
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Se errore 401 e non abbiamo già provato a refreshare
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newToken = await tokenManager.refreshAccessToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh fallito, redirect a login
tokenManager.clearTokens();
window.location.href = "/login";
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);