Guide

Gestire Token

Guida completa per gestire il ciclo di vita dei token e delle API Keys

Questa guida ti aiuta a gestire correttamente il ciclo di vita dei token PASETO e delle API Keys, inclusi refresh, scadenza e gestione errori.

Gestione Token PASETO

Ciclo di Vita dei Token

I token PASETO hanno un ciclo di vita a due livelli:

  1. Access Token: Valido per 1 ora (default)
    • Usato per autenticare le richieste API
    • Scade frequentemente per sicurezza
  2. Refresh Token: Valido per 7 giorni (default)
    • Usato solo per ottenere nuovi access token
    • Scade meno frequentemente

Flusso Completo

1. Login → Ottieni accessToken + refreshToken
2. Usa accessToken per richieste API
3. Quando accessToken scade → Usa refreshToken per ottenere nuovo accessToken
4. Continua con nuovo accessToken
5. Quando refreshToken scade → Riloggare

Rinnovare Access Token

Quando un access token scade, usa il refresh token per ottenerne uno nuovo:

curl -X POST https://nitro.italianonprofit.it/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "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
  }
}

Implementazione Automatica del Refresh

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() {
    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("https://nitro.italianonprofit.it/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 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();
        throw error;
      }
    }

    return response;
  }

  async getAccessToken(): Promise<string> {
    if (!this.accessToken) {
      throw new Error("Not authenticated");
    }
    return this.accessToken;
  }

  private clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    localStorage.removeItem("accessToken");
    localStorage.removeItem("refreshToken");
  }

  logout() {
    this.clearTokens();
  }
}

Refresh Proattivo

Puoi rinnovare il token prima che scada per evitare interruzioni:

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 di Refresh

Refresh Token Scaduto

{
  "status": "error",
  "error": {
    "code": "REFRESH_TOKEN_INVALID",
    "message": "Refresh token non valido o scaduto"
  }
}

Soluzione: L'utente deve rifare login:

if (error.code === "REFRESH_TOKEN_INVALID") {
  tokenManager.logout();
  // Redirect a login
  window.location.href = "/login";
}

Refresh Token Non Valido

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

Soluzione: Rimuovi i token salvati e richiedi nuovo login.

Gestione API Keys

Elenco API Keys

Puoi vedere tutte le tue API Keys (senza il valore completo per sicurezza):

curl -X GET https://nitro.italianonprofit.it/api/v1/api-keys \
  -H "Authorization: Bearer <tuo-access-token>"

Risposta:

{
  "status": "success",
  "data": [
    {
      "id": "uuid-1",
      "name": "Chiave Produzione",
      "prefix": "npf_prefisso1",
      "scopes": ["organizations:read"],
      "expiresAt": "2024-12-31T23:59:59.999Z",
      "createdAt": "2024-01-01T00:00:00.000Z",
      "active": true
    }
  ]
}

Revocare un'API Key

Se un'API Key è stata compromessa o non è più necessaria:

curl -X DELETE https://nitro.italianonprofit.it/api/v1/api-keys/:id \
  -H "Authorization: Bearer <tuo-access-token>"

Dove :id è l'ID dell'API Key da revocare.

Disattivare Temporaneamente un'API Key

Puoi disattivare temporaneamente un'API Key senza eliminarla:

curl -X POST https://nitro.italianonprofit.it/api/v1/api-keys/:id/deactivate \
  -H "Authorization: Bearer <tuo-access-token>"

Questo è utile quando vuoi sospendere un'integrazione senza perdere completamente la chiave.

Rotazione delle API Keys

È buona pratica ruotare regolarmente le API Keys:

async function rotateApiKey(oldApiKeyId: string, newApiKeyName: string) {
  // 1. Crea nuova API Key
  const newApiKey = await createApiKey(newApiKeyName, ["organizations:read"]);

  // 2. Aggiorna il codice per usare la nuova chiave
  // (qui dovresti aggiornare la configurazione della tua applicazione)

  // 3. Attendi qualche giorno per verificare che tutto funzioni

  // 4. Revoca la vecchia API Key
  await revokeApiKey(oldApiKeyId);
}

Gestione Errori di Autenticazione

Token Scaduto (401)

{
  "status": "error",
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "Token non valido o scaduto"
  }
}

Gestione automatica:

async function makeRequestWithAutoRefresh(url: string, options: RequestInit) {
  const tokenManager = new TokenManager();

  try {
    return await tokenManager.makeRequest(url, options);
  } catch (error) {
    if (error.message === "Refresh token expired") {
      // Redirect a login
      window.location.href = "/login";
    }
    throw error;
  }
}

Autenticazione Richiesta (401)

{
  "status": "error",
  "error": {
    "code": "AUTHENTICATION_REQUIRED",
    "message": "Autenticazione richiesta"
  }
}

Soluzione: Verifica che il token o l'API Key siano presenti nell'header Authorization.

Accesso Negato (403)

{
  "status": "error",
  "error": {
    "code": "FORBIDDEN",
    "message": "Accesso negato"
  }
}

Soluzione: Verifica che l'API Key abbia gli scope necessari o che l'utente abbia i permessi richiesti.

Best Practices

Per Token PASETO

  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

Per API Keys

  1. Ruota regolarmente - cambia le chiavi ogni 90-180 giorni
  2. Una chiave per integrazione - usa chiavi separate per diversi servizi
  3. Monitora l'utilizzo - tieni traccia delle chiavi attive
  4. Revoca immediatamente - se una chiave è compromessa
  5. Usa scope minimi - assegna solo i permessi necessari

Esempi Completi

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.logout();
        window.location.href = "/login";
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Gestione API Key in Python

import requests
import os
from datetime import datetime, timedelta

class ApiKeyManager:
    def __init__(self):
        self.api_key = os.environ.get('ITALIANONPROFIT_API_KEY')
        self.created_at = os.environ.get('ITALIANONPROFIT_API_KEY_CREATED_AT')

    def is_expiring_soon(self, days_threshold=30):
        if not self.created_at:
            return False

        created = datetime.fromisoformat(self.created_at)
        expires_at = created + timedelta(days=365)
        days_until_expiry = (expires_at - datetime.now()).days

        return days_until_expiry < days_threshold

    def rotate_if_needed(self, access_token, key_name):
        if self.is_expiring_soon():
            print(f"API Key scade tra {self.days_until_expiry()} giorni. Rotazione consigliata.")
            # Implementa logica di rotazione
            # ...

    def make_request(self, url, method='POST', **kwargs):
        headers = kwargs.get('headers', {})
        headers['Authorization'] = f'Bearer {self.api_key}'
        kwargs['headers'] = headers

        response = requests.request(method, url, **kwargs)

        if response.status_code == 401:
            raise Exception('API Key non valida o scaduta')

        return response

Prossimi Passi