Best Practice di Sviluppo 14 min di lettura 15 dicembre 2025

Progettare architetture software sicure per applicazioni enterprise

Una guida completa alla sicurezza applicativa enterprise: OAuth 2.0 e OIDC per l'autenticazione, JWT con le pratiche corrette, modelli di controllo accessi RBAC e ABAC, difesa delle API secondo OWASP, architettura zero-trust e crittografia dei dati a riposo e in transito.

ME

Marco Esposito

Lead Software Architect

La sicurezza di un'applicazione enterprise non è una feature da aggiungere a fine progetto: è una proprietà architetturale che deve essere progettata fin dal primo ADR. Le violazioni dei dati costano in media 4,88 milioni di dollari per incidente nel 2024 secondo IBM — un costo che incorpora remediation tecnica, notifiche agli utenti, sanzioni regolamentari e danni reputazionali duraturi. Eppure la maggior parte delle vulnerabilità sfruttate in produzione non deriva da attacchi sofisticati: deriva da configurazioni errate di protocolli ampiamente documentati, da JWT validati in modo superficiale, da API che espongono più dati di quanto necessario e da policy di autorizzazione mai revisionate dopo il deploy iniziale.

Il fondamento: OAuth 2.0 e OpenID Connect

OAuth 2.0 è il protocollo di autorizzazione standard per le API moderne. OpenID Connect (OIDC) è uno strato di identità costruito su OAuth 2.0 che aggiunge autenticazione. La distinzione è critica e spesso fraintesa: OAuth autorizza l'accesso alle risorse (questo client può leggere gli ordini?), OIDC autentica l'identità dell'utente (chi è questo utente?). Per applicazioni enterprise moderne, il flow corretto è quasi sempre Authorization Code con PKCE — mai Implicit Flow, deprecato per valide ragioni di sicurezza, e mai il trasferimento di credenziali direttamente al client. PKCE (Proof Key for Code Exchange) previene l'authorization code interception attack: anche se un attaccante intercettasse il codice di autorizzazione, non potrebbe scambiarlo per un token senza il code_verifier originale.

Flusso OAuth 2.0 Authorization Code con PKCE

1. InizializzazioneClient genera code_verifier (random 43-128 chars) e code_challenge (SHA-256 hash)
2. Authorization RequestRedirect a /authorize con client_id, redirect_uri, scope, state e code_challenge
3. Autenticazione utenteAuthorization Server autentica l'utente e mostra il consent screen
4. Authorization CodeRedirect a redirect_uri con authorization_code e state (verificare anti-CSRF)
5. Token ExchangeClient invia code + code_verifier a /token — il server verifica l'hash SHA-256
6. Token ResponseServer restituisce access_token, refresh_token e id_token (OIDC)

JWT: sicurezza oltre la validazione base

I JSON Web Token sono ovunque, ma la loro semplicità apparente nasconde numerose trappole. Un JWT è firmato (JWS) ma non cifrato per default: il payload è semplicemente base64url-encoded e leggibile da chiunque in possesso del token. Questo significa che non vanno mai inseriti nei claim dati sensibili come password, numeri di carte di credito o informazioni di autorizzazione che non devono essere visibili al client. Per i casi in cui la confidenzialità del payload è necessaria, si usa JWE (JSON Web Encryption). L'attacco 'alg: none' — in cui un attaccante forgia un token impostando l'algoritmo di firma a none — è banale da eseguire su librerie mal configurate: è fondamentale vincolare esplicitamente gli algoritmi accettati.

jwt-validation.tstypescript
import { jwtVerify, createRemoteJWKSet } from "jose";

// JWKS remoti: permette la rotazione delle chiavi senza deploy
const JWKS = createRemoteJWKSet(
  new URL("https://auth.example.com/.well-known/jwks.json"),
);

interface ValidatedClaims {
  sub: string;
  email: string;
  roles: string[];
  aud: string;
  iss: string;
  exp: number;
}

async function validateAccessToken(token: string): Promise<ValidatedClaims> {
  const { payload } = await jwtVerify(token, JWKS, {
    // Vincola issuer atteso — previene token da altri authorization server
    issuer: "https://auth.example.com",
    // Vincola audience attesa — previene audience confusion attacks
    audience: "api.example.com",
    // Forza algoritmi asimmetrici — previene algorithm confusion (alg: "none")
    algorithms: ["RS256", "ES256"],
  });

  if (!payload.sub || typeof payload.sub !== "string") {
    throw new Error("Token privo di subject valido");
  }

  const roles = Array.isArray(payload["roles"])
    ? (payload["roles"] as string[])
    : [];

  return {
    sub: payload.sub,
    email: payload["email"] as string,
    roles,
    aud: Array.isArray(payload.aud) ? payload.aud[0] : (payload.aud as string),
    iss: payload.iss as string,
    exp: payload.exp as number,
  };
}

// Middleware per proteggere gli endpoint API
export async function authMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Token mancante" });
  }
  try {
    const token = authHeader.slice(7);
    const claims = await validateAccessToken(token);
    (req as any).user = claims;
    next();
  } catch (err) {
    // Log dell'errore senza esporre dettagli al client
    logger.warn("JWT validation failed", { error: (err as Error).message });
    return res.status(401).json({ error: "Token non valido o scaduto" });
  }
}
  • Usa sempre JWKS remoti invece di segreti hardcoded: permette la rotazione delle chiavi senza deploy.
  • Vincola esplicitamente gli algoritmi accettati: l'attacco 'alg: none' è banale su librerie mal configurate.
  • Imposta exp breve (15-60 minuti) per gli access token e usa refresh token rotation per le sessioni longeve.
  • Non inserire PII sensibili nel payload JWT: il contenuto è solo base64, non cifratura.
  • Implementa token revocation tramite una blocklist Redis per i casi di logout forzato o compromise.

Modelli di controllo accessi: RBAC, ABAC e il modello ibrido

Role-Based Access Control (RBAC) è il modello più diffuso: ogni utente ha uno o più ruoli e ogni ruolo ha un insieme di permessi. È semplice da implementare e da auditare, ma soffre di 'role explosion' man mano che l'organizzazione cresce. Attribute-Based Access Control (ABAC) valuta le decisioni di accesso basandosi su attributi dell'utente (reparto, livello di seniority), dell'ambiente (orario, posizione geografica, device posture) e della risorsa (classificazione del dato, owner). ABAC è più espressivo ma più complesso da governare. La best practice enterprise moderna è un modello ibrido: RBAC come base per i permessi standard, arricchito con policy ABAC per i casi che richiedono contesto dinamico.

authorization-service.tstypescript
// Modello ibrido RBAC + ABAC con CASL
import {
  AbilityBuilder,
  createMongoAbility,
  MongoAbility,
} from "@casl/ability";

type Actions = "read" | "create" | "update" | "delete" | "approve";
type Subjects = "Order" | "Invoice" | "Report" | "User" | "all";
type AppAbility = MongoAbility<[Actions, Subjects]>;

interface UserContext {
  id: string;
  roles: string[];
  department: string;
  clearanceLevel: number; // 1-5
}

export function defineAbilitiesFor(user: UserContext): AppAbility {
  const { can, cannot, build } = new AbilityBuilder<AppAbility>(
    createMongoAbility,
  );

  if (user.roles.includes("admin")) {
    can("manage", "all");
  }

  if (user.roles.includes("manager")) {
    can("read", "Order");
    can("read", "Invoice");
    // ABAC: approva solo ordini sotto soglia (attributo della risorsa)
    can("approve", "Order", { amount: { $lte: 50000 } });
  }

  if (user.roles.includes("analyst")) {
    can("read", "Report");
    can("read", "Order");
    // ABAC: crea report solo per il proprio dipartimento
    can("create", "Report", { department: user.department });
  }

  if (user.roles.includes("viewer")) {
    can("read", "Order");
    // ABAC: leggi solo report con clearance <= clearance utente
    can("read", "Report", {
      clearanceRequired: { $lte: user.clearanceLevel },
    });
  }

  // Regola esplicita di negazione — audit trail immutabile
  cannot("delete", "Invoice");

  return build({
    detectSubjectType: (item) => item.constructor.name as Subjects,
  });
}

export async function approveOrder(req: Request, res: Response) {
  const ability = defineAbilitiesFor((req as any).user);
  const order = await Order.findById(req.params.id);

  if (!order || ability.cannot("approve", order)) {
    return res.status(403).json({ error: "Permesso negato" });
  }

  await order.approve((req as any).user.id);
  res.json({ status: "approved" });
}

OWASP API Security Top 10: le vulnerabilità da non ignorare

L'OWASP API Security Top 10 è la lista di riferimento per le vulnerabilità più critiche delle API moderne. Le prime tre posizioni — Broken Object Level Authorization (BOLA), Broken Authentication e Broken Object Property Level Authorization — condividono una causa comune: fidarsi dell'input del client per determinare cosa mostrare o modificare. BOLA significa che un utente può leggere o modificare risorse di altri utenti semplicemente cambiando un ID nell'URL. La correzione è sempre verificare, lato server, che la risorsa richiesta appartenga all'utente autenticato — mai assumere che l'ID nel token JWT corrisponda all'ID nell'URL.

  • BOLA (API1): verifica sempre che req.user.id === resource.ownerId prima di restituire o modificare dati.
  • Broken Authentication (API2): usa PKCE, ruota i refresh token, implementa rate limiting su /token.
  • Broken Object Property Level Authorization (API3): usa DTO espliciti e non esporre mai l'intera entity ORM.
  • Unrestricted Resource Consumption (API4): rate limiting per IP e per utente su tutti gli endpoint pubblici.
  • Broken Function Level Authorization (API5): le route admin devono avere middleware di autorizzazione separato.
  • Unrestricted Access to Sensitive Business Flows (API6): limita operazioni critiche con CAPTCHA e anomaly detection.
  • Server Side Request Forgery — SSRF (API7): valida e applica una whitelist a qualsiasi URL fornito in input.
  • Security Misconfiguration (API8): rimuovi endpoint di debug, disabilita CORS wildcard, aggiorna le dipendenze.

Zero Trust: non fidarsi mai, verificare sempre

Il modello Zero Trust ribalta l'assunzione tradizionale che le reti interne siano sicure. In un'architettura Zero Trust, ogni richiesta — anche proveniente da un servizio interno — viene autenticata e autorizzata esplicitamente. Non esiste una 'zona sicura'. I principi operativi sono: verifica continua dell'identità (ogni chiamata API porta un token verificato), accesso con privilegio minimo (ogni servizio ha solo i permessi strettamente necessari), micro-segmentazione della rete (i servizi non possono comunicare liberamente) e assunzione di breach (monitora e logga tutto come se ci fosse già un attaccante dentro). Service mesh come Istio o Linkerd implementano mutual TLS automaticamente tra i pod Kubernetes, garantendo che ogni comunicazione service-to-service sia cifrata e autenticata.

Crittografia: dati a riposo e in transito

TLS 1.3 è il requisito minimo per tutti i dati in transito — TLS 1.0 e 1.1 sono deprecati e vulnerabili. Per le API interne su Kubernetes, mTLS via service mesh è la soluzione raccomandata. Per i dati a riposo, la strategia dipende dal livello di sensibilità: la cifratura a livello di disco protegge da attacchi fisici ma non dall'applicazione stessa. La cifratura a livello di campo — cifrare specifiche colonne del database con chiavi gestite dall'applicazione tramite AWS KMS o HashiCorp Vault — fornisce difesa in profondità: anche un dump del database non espone i dati sensibili. Le chiavi di cifratura non devono mai essere hardcoded nel codice o nelle variabili di ambiente in chiaro: usa un secret manager dedicato con rotation automatica.

Sicurezza pratica: esegui un threat model esplicito per ogni nuova feature che tocca dati sensibili o flussi di autenticazione. Il modello STRIDE (Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege) è un framework efficace per identificare sistematicamente le superfici di attacco prima che diventino vulnerabilità in produzione.

Conclusioni

La sicurezza enterprise non è un prodotto che si acquista o una checklist che si spunta: è una disciplina ingegneristica continua. OAuth 2.0 con PKCE gestisce l'autorizzazione in modo standard e auditabile; JWT validati correttamente riducono drasticamente la superficie di attacco dei token; RBAC con overlay ABAC bilancia semplicità operativa e granularità delle policy; OWASP API Top 10 fornisce la lista di priorità per le revisioni di sicurezza; Zero Trust con mTLS elimina il perimetro come concetto di sicurezza; la crittografia dei dati sensibili con gestione delle chiavi esternalizzata completa la difesa in profondità. Ognuno di questi layer, da solo, è insufficiente. Insieme, formano un'architettura che rende costoso e difficile per un attaccante ottenere accesso significativo anche quando un singolo controllo fallisce.

Tag

SicurezzaOAuth 2.0OIDCJWTRBACABACZero TrustOWASPAPI SecurityCrittografia

Prossimo passo

Hai bisogno di implementare questa architettura?

Il nostro team di ingegneria costruisce e scala i sistemi descritti in questo articolo. Dalla discovery alla produzione — con risultati misurabili.