Autenticazione JWT in Symfony 7: API sicure per i portali clienti
Un portale clienti che espone un'API ha bisogno di un'autenticazione robusta, e il JSON Web Token è la scelta naturale per gli scenari stateless: nessuna sessione server da mantenere, il token che il client porta con sé a ogni richiesta. Il problema è che il JWT mal implementato è una delle falle più classiche che trovo negli audit, e non perché il JWT sia insicuro in sé, ma perché è facile usarlo male in modi che sembrano funzionare. Un token che non scade mai, una chiave di firma debole, una verifica della firma che si può aggirare: tutti difetti invisibili in produzione, perché l'applicazione funziona perfettamente, finché un attaccante non li sfrutta. In un audit di sicurezza di un'API JWT, il primo posto dove guardo non è se l'autenticazione funziona, è come verifica i token, perché è lì che si nascondono i problemi. Vediamo come implementare l'autenticazione JWT bene in Symfony 7, dalla firma alla scadenza alla revoca, e poi mettiamoci dal lato di chi quei token cerca di forgiarli, perché è l'unico modo per capire cosa rende un JWT davvero sicuro.
Cos'è un JWT, e dove sta davvero il rischio
Un JWT è composto da tre parti separate da un punto: un header che dichiara l'algoritmo di firma, un payload che contiene le informazioni (i claim, come l'identità dell'utente e la scadenza), e una firma che lega le prime due. La cosa fondamentale da capire, e che vedo fraintesa di continuo, è che il payload non è cifrato, è solo firmato. Chiunque abbia il token può leggerne il contenuto decodificandolo: la firma garantisce che non sia stato modificato, non che sia segreto.
Da qui discendono due verità che orientano tutto. La prima: non si mettono mai dati sensibili nel payload di un JWT, perché sono leggibili da chiunque intercetti il token. La seconda, più importante: tutta la sicurezza di un JWT sta nella firma. Se la firma è forte e viene verificata correttamente, il token non è falsificabile; se la firma è debole o la verifica è aggirabile, l'intero schema crolla, perché un attaccante può forgiare un token che dice quello che vuole, per esempio "sono l'amministratore". Il rischio del JWT, quindi, non è nel concetto, è nell'implementazione della firma e della sua verifica.
Un JWT firma il messaggio, non lo nasconde. Mettere una password o un numero di carta nel payload "tanto è un token" è come scrivere un segreto su una cartolina e fidarsi del timbro postale: il timbro dimostra che non è stata alterata, ma la cartolina la legge chiunque la maneggi.
Implementarlo bene in Symfony 7
In Symfony lo standard di fatto per il JWT è il bundle LexikJWTAuthenticationBundle, che si integra con il componente Security e gestisce emissione e verifica secondo lo standard RFC 7519. La scelta che faccio sempre, all'inizio, è l'algoritmo di firma. Per un portale clienti preferisco la firma asimmetrica RS256, basata su una coppia di chiavi: il server firma con la chiave privata e verifica con la chiave pubblica. Il vantaggio rispetto alla firma simmetrica HS256, dove la stessa chiave firma e verifica, è che la chiave di verifica può essere distribuita senza dare a nessuno il potere di firmare nuovi token.
# config/packages/security.yaml (estratto concettuale)
firewalls:
api_login:
pattern: ^/api/login
stateless: true
json_login:
check_path: /api/login
success_handler: lexik_jwt_authentication.handler.authentication_success
api:
pattern: ^/api
stateless: true
jwt: ~Con questa configurazione, l'endpoint di login restituisce un token alla coppia credenziali valida, e tutte le rotte sotto ^/api lo verificano automaticamente, in modo stateless. Le chiavi vanno generate con una passphrase robusta, conservate fuori dal codice (nelle variabili d'ambiente o in un vault, mai nel repository), e protette come qualunque segreto critico. Questa è la base; il resto della sicurezza sta nei dettagli di scadenza, revoca e nel chiudere i vettori che un attaccante prova.
Se gestisci un portale clienti con autenticazione su API e vuoi sapere quanto regge a un tentativo di forgiare i token, nel mio profilo professionale trovi l'esperienza concreta di penetration testing e audit di autenticazione su applicazioni Symfony in produzione.
Scadenza e refresh token: il compromesso tra sicurezza e usabilità
Un JWT andrebbe sempre emesso con una scadenza breve, perché è un token che, una volta firmato, è valido fino alla scadenza e non lo puoi richiamare indietro facilmente. Una scadenza breve, dell'ordine di minuti o poche decine di minuti, limita la finestra in cui un token rubato è utile. Ma una scadenza breve, da sola, rovinerebbe l'esperienza: l'utente verrebbe sloggato di continuo. La soluzione è la coppia access token e refresh token: l'access token è il JWT a vita breve che autentica le richieste; il refresh token è un token a vita più lunga, conservato in modo sicuro, che serve solo a ottenere un nuovo access token quando quello scade.
La differenza di sicurezza tra i due è cruciale. L'access token è stateless e a vita breve, quindi il rischio di un suo furto è contenuto dalla scadenza. Il refresh token, a vita lunga, va invece trattato come un segreto di valore: conservato lato server in modo da poterlo revocare, associato all'utente e al dispositivo, e invalidato al logout o al sospetto di compromissione. È questo il design che concilia sicurezza e usabilità: token di accesso usa-e-getta, token di refresh revocabile e custodito. Una buona pratica aggiuntiva è la rotazione del refresh token a ogni rinnovo: ogni volta che lo usi per ottenere un nuovo access token, ne emetti anche uno nuovo di refresh e invalidi il precedente, così un refresh token intercettato e usato dall'attaccante viene scoperto al primo uso legittimo, perché risulta già consumato.
Il problema della revoca: il tallone d'Achille del JWT
Qui sta il limite intrinseco del JWT stateless, ed è il punto che separa chi lo capisce da chi lo usa a scatola chiusa. Un JWT firmato e valido non si può revocare prima della sua scadenza, perché il server non tiene stato: verifica la firma e la scadenza, e se sono a posto accetta il token, senza consultare nessun elenco. Se un token viene rubato, resta valido fino alla scadenza qualunque cosa tu faccia, a meno di non introdurre uno stato.
Le soluzioni sono tre, ognuna con il suo compromesso. La prima è la scadenza molto breve, che riduce la finestra di rischio senza aggiungere stato, ed è perché la scadenza breve è così importante. La seconda è una denylist dei token revocati, un elenco (tipicamente in Redis, con TTL pari alla scadenza del token) degli identificatori jti dei token invalidati, che il server consulta a ogni richiesta: reintroduce un po' di stato e un po' di latenza, ma restituisce la capacità di revoca immediata. La terza è l'approccio ibrido, access token stateless a vita brevissima più refresh token stateful e revocabile, che è quello che raccomando per la maggior parte dei portali, perché bilancia le due esigenze. La scelta dipende da quanto è critica la revoca immediata per il tuo caso: per un portale che tratta dati sensibili, la denylist o l'ibrido valgono il costo.
Dal lato attaccante: cosa trova un audit JWT
Ecco la parte che rende sicura l'implementazione, perché la guarda da fuori. Quando faccio l'audit di un JWT, cerco una lista precisa di difetti. Il primo, e il più grave, è l'algorithm confusion. Storicamente alcune librerie accettavano un token con alg: none, cioè senza firma, fidandosi di un attaccante che dichiarava "questo token non è firmato": un disastro. La variante più subdola è la confusione RS256-HS256: se il server usa RS256 (chiave pubblica per verificare) ma la libreria può essere ingannata a verificare con HS256, un attaccante può firmare un token con HMAC usando la chiave pubblica (che è pubblica per definizione) come segreto, e il server lo accetta. La difesa è imporre esplicitamente l'algoritmo atteso nella verifica e non lasciare che sia l'header del token a deciderlo.
Il secondo difetto è la chiave di firma debole: con HS256, se il segreto è una parola corta o prevedibile, si può fare brute force offline sulla firma e poi forgiare token a volontà. Il terzo sono i token che non scadono, o con scadenza lunghissima, che trasformano un furto temporaneo in un accesso permanente. Il quarto sono i dati sensibili nel payload, leggibili da chiunque. Il quinto è la verifica della firma assente o difettosa, dove l'applicazione decodifica il payload ma non verifica davvero che la firma sia valida. Questi controlli si possono fare in modo sistematico, ed esiste anche uno strumento dedicato: nel mio sito metto a disposizione un audit JWT che ispeziona un token e segnala questa classe di problemi, lo stesso approccio metodico che applico negli audit più ampi sulle vulnerabilità OAuth2 e di implementazione delle API.
JWT contro sessione: quando il JWT è la scelta sbagliata
Prima di immergersi nei dettagli vale la pena fare un passo indietro e chiedersi se il JWT sia davvero lo strumento giusto, perché la risposta non è sempre sì. Il JWT brilla quando hai bisogno di autenticazione stateless e cross-domain: un'API consumata da client diversi, un'architettura a microservizi dove più servizi devono verificare l'identità senza condividere una sessione, un'app mobile che parla con un backend separato. In questi scenari il fatto che il token sia autoconsistente e verificabile senza consultare uno store centrale è un vantaggio reale.
Ma se il tuo "portale clienti" è in realtà un'applicazione web monolitica servita dallo stesso dominio, dove il frontend e il backend vivono insieme, la sessione server classica è spesso la scelta migliore, ed è quella che il framework offre già pronta. Una sessione è revocabile per costruzione (la cancelli e l'utente è fuori), non soffre del problema della revoca che affligge il JWT, e non ti carica della responsabilità di gestire firma, scadenza e refresh. Scegliere il JWT per riflesso, perché "le API moderne usano il JWT", è la stessa forma di over-engineering che vedo nella scelta degli strumenti di autenticazione in generale: si adotta la soluzione più complessa per moda, e si eredita un problema, la revoca, che con la sessione semplicemente non esisteva. La regola è la solita: il JWT quando lo stateless ti serve davvero, la sessione quando il tuo è un classico monolite web. Sceglierlo con cognizione è già metà della sicurezza.
Dove si conserva un JWT lato client?
È la domanda che decide metà della sicurezza pratica, e non ha una risposta unica. Le due opzioni classiche sul web sono il localStorage e il cookie. Il localStorage è comodo ma è leggibile dal JavaScript, quindi una singola vulnerabilità XSS espone il token a furto: per un dato di autenticazione è un rischio serio. Il cookie httpOnly, al contrario, non è accessibile al JavaScript, quindi è al riparo dall'XSS, ma è soggetto al rischio CSRF, che però Symfony e i token di protezione gestiscono bene. La mia raccomandazione, per un portale web, è il cookie httpOnly e Secure, con la protezione CSRF attiva, perché sposta il token fuori dalla portata dell'XSS, che è il vettore più comune. Per un'app mobile valgono invece gli archivi sicuri del sistema operativo (il Keychain su iOS, il Keystore su Android), mai un file in chiaro o una preferenza condivisa non cifrata, che un dispositivo compromesso o un backup non protetto renderebbero leggibili. La regola di fondo è non lasciare mai un token di autenticazione dove un attacco comune può leggerlo, e l'XSS è il più comune di tutti, come ho ribadito parlando del componente Security di Symfony.
Tirando le somme, l'autenticazione JWT in Symfony 7 è una soluzione eccellente per un portale clienti, a patto di trattarla per quello che è: uno schema in cui tutta la sicurezza vive nella firma e nei dettagli di gestione, non nella comodità dello stateless. Si firma con un algoritmo forte e una chiave custodita come un segreto, si emettono access token a vita breve accompagnati da refresh token revocabili, si affronta apertamente il limite della revoca scegliendo tra scadenza breve, denylist e approccio ibrido, e si conservano i token dove un attacco comune non li raggiunge. Soprattutto, si verifica il proprio JWT dal lato di chi vuole forgiarlo, perché l'algorithm confusion, le chiavi deboli e le verifiche difettose sono difetti che non si vedono dall'interno e si vedono benissimo da fuori. Se gestisci un portale clienti su API e vuoi la certezza che la sua autenticazione regga a un tentativo serio di aggirarla, contattami per una consulenza diretta: nella mia esperienza, un'implementazione JWT che sembra solida e una che lo è davvero si distinguono solo guardandole con gli occhi dell'attaccante.