DIEGO CECATO / NOTE DAL CAMPO

Il login non basta: Symfony 8.2 chiede prove, non fiducia

diego cecato//
Copertina: Il login non basta: Symfony 8.2 chiede prove, non fiducia

Un utente entra in un’applicazione con la propria password, apre la dashboard, consulta le fatture e poi decide di cambiare l’IBAN sul quale ricevere un rimborso. La sessione è valida, il cookie è firmato, il firewall ha riconosciuto l’identità. Tutto regolare. Eppure, proprio nell’ultimo passaggio, manca un’informazione decisiva: quale prova di identità abbiamo davvero ottenuto e quanto tempo fa?

È uno di quei difetti di progettazione che non si vedono nei diagrammi con la casella “utente autenticato” colorata di verde. Il sistema ha risposto alla domanda più semplice, “chi sei?”, e usa quella risposta per risolverne un’altra, molto più delicata: “puoi eseguire questa operazione adesso?”. Sono domande diverse, e trattarle come sinonimi è una scorciatoia che prima o poi presenta il conto.

Con le funzionalità in sviluppo per Symfony 8.2, il framework prova a rendere esplicita questa differenza: non soltanto una sessione recente, ma le prove dei metodi di autenticazione effettivamente usati, con un riferimento temporale per ciascuno. La novità interessante non è una nuova schermata di login. È la possibilità di prendere decisioni di autorizzazione meno grossolane.

Una sessione non è una cambiale in bianco

La maggior parte delle applicazioni distingue già tra utente anonimo e utente autenticato, magari aggiungendo ruoli e permessi. Il problema emerge quando due operazioni con rischi radicalmente diversi attraversano lo stesso controllo. Leggere un documento e disporre un pagamento possono essere azioni consentite allo stesso account, ma non dovrebbero necessariamente richiedere la stessa dimostrazione di identità.

Un accesso effettuato al mattino con password può essere sufficiente per consultare lo storico. Per cambiare un beneficiario, invece, si può richiedere una verifica recente con una chiave hardware o un metodo più forte. Il ruolo autorizza la funzione; la prova di autenticazione determina se, in quel momento, esistono le condizioni per esercitarla. Confondere i due livelli significa lasciare alla durata della sessione una responsabilità che non le appartiene.

È lo stesso errore concettuale che ritroviamo quando consideriamo sufficiente un indicatore formalmente valido per attribuirgli una fiducia più ampia. Ne ho parlato anche ragionando su certificati e identità: la validità di una prova non autorizza automaticamente conclusioni che quella prova non contiene.

Il tempo conta, ma bisogna sapere di quale prova stiamo parlando

Una soluzione superficiale consiste nel chiedere se l’utente ha effettuato il login negli ultimi cinque minuti. È meglio di niente, ma non basta. Se il sistema registra soltanto l’ultimo istante di autenticazione, una nuova richiesta della password può aggiornare quel timestamp senza dimostrare che l’utente abbia usato anche il fattore richiesto per l’operazione sensibile.

Immaginiamo che alle 9:00 sia stata verificata una chiave hardware e alle 15:00 venga inserita nuovamente la password. Alle 15:02 la sessione è certamente recente; la prova relativa alla chiave, però, ha sei ore. Se il requisito è una chiave hardware usata negli ultimi cinque minuti, il secondo evento non dovrebbe rinfrescare artificialmente il primo. Serve una cronologia distinta per metodo, non un unico bollino “autenticato di recente”.

Questa distinzione rende più preciso anche il comportamento dell’interfaccia. Non occorre chiedere sempre tutto a tutti: si può intervenire quando l’operazione lo richiede, spiegando perché quella specifica prova non è sufficiente o non è più fresca. La sicurezza smette di essere un ostacolo uniforme e diventa una regola applicata nel punto giusto.

Che cosa introduce Symfony 8.2

Il lavoro descritto dal team Symfony permette al token di sicurezza di esporre una mappa delle prove di autenticazione attraverso getAuthenticationProofs(): metodi e timestamp dell’ultima dimostrazione. I nomi dei metodi seguono la terminologia di RFC 8176, utile per non inventare convenzioni incompatibili tra applicazione e identity provider.

Gli authenticator nativi per password possono registrare il metodo corrispondente. Un authenticator personalizzato può dichiarare le prove effettivamente raccolte tramite AuthenticationMethodBadge. In uno scenario OpenID Connect, invece, la claim amr può descrivere i metodi utilizzati dal provider. Questo non elimina la necessità di verificare la fiducia nel provider e la correttezza del flusso: trasferisce al codice applicativo informazioni più ricche, non una verità magica.

Il dettaglio da non perdere è che si tratta di capacità presentate per il ramo Symfony 8.2, non di una promessa che ogni installazione in produzione le possieda già. Prima di progettare una migrazione occorre controllare versione, componenti e comportamento concreto degli authenticator in uso. Il nome di una funzionalità in una release note non sostituisce un test d’integrazione.

La ri-autenticazione non è un 403 mascherato

La parte architetturalmente interessante non è inventare una terza risposta universale per ogni voter. È distinguere il diritto di eseguire un’operazione dalla qualità della prova necessaria per esercitarlo. Symfony 8.2 introduce IS_AUTHENTICATED_RECENTLY e IS_AUTHENTICATED_VERY_RECENTLY, controlli temporali che si affiancano ai permessi applicativi. Il secondo livello non rende più permissivo il primo.

Quando manca una autenticazione abbastanza recente, la documentazione di Symfony descrive un ReAuthenticationEntryPointInterface che può avviare un nuovo passaggio di verifica invece di lasciare l’utente davanti a un diniego senza percorso di recupero. Con l’autenticatore OpenID Connect introdotto nella stessa versione, il flusso può tornare al provider usando prompt=login. Non significa che qualunque diniego di un voter debba trasformarsi in una richiesta di credenziali: un permesso negato rimane negato.

Per il codice applicativo la conseguenza è concreta: mantenere i voter responsabili dei diritti e progettare separatamente i requisiti di freschezza e di metodo, con test che coprano sia il recupero sia il rifiuto definitivo. Il controller deve restare leggibile, ma nessuna astrazione può sostituire la scelta esplicita di quali operazioni meritino una prova aggiuntiva.

Autorizzare un’operazione non significa fidarsi della sessione in generale. Significa sapere quale prova serve per quella decisione, e poter dimostrare che è ancora valida.

OpenID Connect: chiedere un livello non equivale a ottenerlo

Quando l’autenticazione è delegata a un identity provider, la tentazione è pensare che basti inviare il parametro corretto. In OpenID Connect, acr_values consente di richiedere un contesto di autenticazione più forte. Ma una richiesta non è una garanzia di esecuzione: il provider può avere comportamenti e livelli supportati differenti. L’applicazione deve verificare il valore acr realmente restituito, oltre alle altre verifiche del token.

Anche amr e acr rispondono a domande diverse. La prima può indicare i metodi impiegati; la seconda rappresenta il contesto o livello di autenticazione secondo la convenzione del provider. Non sono etichette intercambiabili e non andrebbero confrontate come stringhe senza una policy esplicita. Una chiave hardware dichiarata dal provider non è automaticamente una prova fresca per ogni operazione, così come un livello richiesto non è un livello ottenuto.

La documentazione OIDC di Symfony 8.2 chiarisce inoltre il ruolo di auth_time, max_age e della ri-autenticazione con prompt=login. È una lettura utile proprio perché costringe a separare la richiesta al provider dalla verifica lato applicazione. Se il provider ignora un invito a ripetere il login, il controllo del tempo dichiarato nel token deve impedire che una vecchia autenticazione passi per nuova.

Prima del codice serve una matrice delle operazioni

Il rischio, davanti a una nuova API, è partire dal metodo da chiamare. Io partirei da una tabella molto meno entusiasmante: operazione, danno potenziale, metodo richiesto, età massima della prova, percorso di recupero e comportamento quando la prova manca. Consultare una fattura, cambiare email, aggiungere un dispositivo fidato, esportare dati personali e autorizzare un pagamento non dovrebbero finire automaticamente nella stessa riga.

Poi bisogna definire il fallimento. Se la chiave hardware non è disponibile, l’operazione viene bloccata? Esiste un metodo alternativo approvato? Si deve mostrare un messaggio specifico? E come si evita un ciclo infinito di redirect quando il provider non soddisfa il livello richiesto? Sono decisioni di prodotto e di rischio, non dettagli che il framework può indovinare.

La stessa disciplina vale per gli altri confini di fiducia di un’applicazione. In un’analisi sui confini di sicurezza in Django il punto era analogo: il problema non è soltanto il controllo mancante, ma l’assunzione implicita che un dato o un passaggio sia affidabile perché proviene da una zona considerata sicura. Qui quella zona è la sessione autenticata.

I test che distinguono una policy da un’illusione

Una suite seria non si limita al caso felice in cui l’utente presenta il fattore corretto. Deve provare almeno il login recente con metodo sbagliato, il metodo giusto ma troppo vecchio, una ri-autenticazione che aggiorna soltanto la password, un provider che non restituisce acr coerente con la richiesta e un voter che nega l’accesso per una ragione non recuperabile. In quest’ultimo caso non deve comparire una scorciatoia verso la ri-autenticazione: manca il diritto, non la freschezza della prova.

Vanno controllati anche i bordi: più tab del browser aperti, sessioni ricordate, refresh del token, logout, cambiamento del fattore e ritorno all’azione originale. Non basta che il flusso sia elegante in una demo. Deve essere prevedibile quando l’utente interrompe il passaggio, quando il provider risponde diversamente dal previsto e quando l’applicazione incontra un errore.

La sicurezza ben progettata non coincide con il massimo numero di richieste di autenticazione. Coincide con il minimo insieme di prove sufficienti, richieste nel momento corretto e controllate in modo verificabile. È una differenza che migliora contemporaneamente protezione e usabilità, senza chiedere agli utenti di superare ostacoli inutili.

La lezione che resta anche senza Symfony

Symfony 8.2 offre strumenti interessanti per rendere esplicita una distinzione che ogni sistema con operazioni sensibili dovrebbe conoscere. Ma il punto non dipende da PHP, da un voter o da un protocollo specifico. Un’identità riconosciuta non è un’autorizzazione illimitata; un fattore usato ieri non è un fattore verificato ora; una richiesta inviata a un provider non è una prova ricevuta.

Quando progettiamo un sistema, ci piace ridurre tutto a stati semplici: autenticato o no, autorizzato o no. La realtà richiede talvolta una terza dimensione, la qualità e la freschezza della prova. Ignorarla rende il codice più corto, non necessariamente il sistema più sicuro. E se per evitare qualche riga di policy dobbiamo fingere che tutte le operazioni abbiano lo stesso rischio, forse il problema non è la complessità del framework.

Domande frequenti

Che differenza c'è tra sessione recente e metodo di autenticazione recente?

La sessione recente indica quando è avvenuta una autenticazione; la prova per metodo permette di verificare quando è stato effettivamente usato uno specifico fattore, per esempio una chiave hardware. Le due informazioni non sono intercambiabili.

Symfony 8.2 obbliga a usare una chiave hardware?

No. Offre strumenti per registrare e controllare le prove dei metodi di autenticazione. È l’applicazione a definire quali metodi e quale freschezza richiedere per ogni operazione sensibile.

Quando Symfony 8.2 può chiedere una nuova autenticazione invece di mostrare un errore 403?

Quando il controllo di accesso richiede una autenticazione recente e questa manca, un entry point di ri-autenticazione può avviare un nuovo login. Non trasforma in recuperabili i dinieghi dovuti a permessi realmente mancanti.

Basta inviare acr_values al provider OpenID Connect?

No. acr_values è una richiesta di livello di autenticazione. L’applicazione deve verificare il valore acr effettivamente restituito e applicare la propria policy, senza presumere che il provider abbia soddisfatto la richiesta.