Autenticazione

Aggiornamento Token

Come rinnovare i token di accesso usando i refresh token

Questa guida spiega come gestire correttamente il rinnovo dei token di accesso quando scadono.

Panoramica

Il sistema utilizza un meccanismo a doppio token:

  • Access Token: Valido per 1 ora (default), usato per autenticare le richieste
  • Refresh Token: Valido per 7 giorni (default), usato solo per ottenere nuovi access token

Quando un access token scade, puoi usare il refresh token per ottenere un nuovo access token senza dover rifare login.

Endpoint di Refresh

Rinnovare Access Token

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
  }
}

Gestione Automatica del Refresh

Implementazione Client-Side

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

Gestione Scadenze

Verifica Scadenza Token

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

Refresh Proattivo

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

Gestione Errori

Refresh Token Scaduto

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";
}

Refresh Token Non Valido

Se il refresh token non è valido o è stato revocato:

{
  "status": "error",
  "error": {
    "code": "INVALID_TOKEN",
    "message": "Token non valido"
  }
}

Best Practices

  1. Salva sempre entrambi i token dopo il login
  2. Usa refresh token solo per refresh - non per autenticare richieste
  3. Gestisci errori di refresh - redirect a login se necessario
  4. Evita chiamate multiple - usa un meccanismo di debouncing
  5. Refresh proattivo - rinnova prima della scadenza quando possibile
  6. Salva in secure storage - usa httpOnly cookies o secure storage per produzione

Esempio Completo con Interceptor

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

Prossimi Passi