La protezione delle risorse web tramite sistemi di autenticazione e autorizzazione è un elemento che non si può prescindere durante lo sviluppo di applicazioni web. Ne ha parlato recentemente anche il nostro Mauro in un , perché è un tema molto sentito. Un sistema di autenticazione per applicativi web può essere realizzato in modo stateless o statefull. Vista la natura senza stato dell’architettura REST, riuscire a realizzare un sistema di autenticazione stateless è molto interessante, ma come fare, soprattuto nel mondo Java? Sembra che con i JSON Web Token (JWT – specifica ancora in draft) si possa risolvere il problema in modo semplice e funzionale.
Sistemi di autenticazione Stateless vs Stateful
Quando si pensa all’autenticazione web, la prima cosa che viene in mente è una form di login. Dietro le quinte però cosa succede? Come fa il server a riconoscere ogni mia chiamata? Dove stanno le informazioni della mia sessione utente? La risposta varia a seconda del sistema di autenticazione.
- Stateless
-
in questa categoria rientra per esempio l’autenticazione BASIC: ad ogni chiamata client-server viene passato un token di autenticazione nell’header HTTP, così che ogni chiamata viene riautenticata. Se mantengo una sorta di cache delle autenticazioni, ne guadagno in performance ma divento stateful.
Un altro tipo di autenticazione è quella basata su token firmati lato server, come per esempio il caso di JWT che approfondiremo, contenente tutte le informazioni della sessione utente. La responsabilità di gestire lo stato della comunicazione è demandata quindi al client: il server verificherà l’autenticità del token in base alla firma.
- Stateful
-
l’autenticazine FORM ne è un classico esempio: lato server viene generato un cookie, passato automaticamente ad ogni richiesta client-server, e memorizzato nell’elenco delle sessioni attive sul server.
Altri tipi di autenticazione più orientate ai web services sono OAuth e OAuth2: come l’autenticazione BASIC, sono basate su token nell’header HTTP ma necessitano del mantenimento “in memoria” (o chi per essa) di un elenco di coppie token/utenti per riconoscere il chiamante.
Viva lo Stateless!
Lo sviluppo di applicativi web oggigiorno è orientato sempre di più ai servizi: dal momento che le interfacce non sono più solamente web ma anche mobile, scrivere una sola volta la logica di business ed incapsularla in servizi web fa risparmiare tempo (e denaro…). In questo contesto, si è affermata l’architettura REST per svariati motivi (che richiederebbero maggiori argomentazioni). A mio avviso però, l’elemento vincente è stata l’introduzione a livello applicativo del concetto di stateless: l’aver “elevato” da protocollo di trasporto a protocollo applicativo l’HTTP, ha introdotto nelle applicazioni una serie di caratteristiche tipiche del protocollo stesso, tra cui il fatto di non tenere lo stato della comunicazione tra una richiesta e l’altra. Se il server è senza stato, può scalare molto più facilmente, e sicuramente sarà molto più semplice perché elimina alla radice il problema della gestione e sincronizzazione delle sessioni utente in ambienti clusterizzati.
Lo stesso discorso vale per i sistemi di autenticazione, siano essi standalone o integrati alla nostra applicazione. Se vogliamo essere ligi all’architettura REST, dobbiamo scartare l’autenticazione FORM e OAuth perché il server deve mantenere uno stato per riconoscere il cookie o il token proveniente dal client. L’autenticazione BASIC usata in modo totalmente stateless crea un’overhead di autenticazioni lato server ad ogni richiesta, quindi sarebbe meglio tenere una cache degli utenti autenticati. Quindi meglio stateful di stateless? E la scalabilità? E i problemi di memoria? Come si esce da questo impasse?
La soluzione potrebbe essere delegare totalmente lo stato della sessione utente al client, ma rimane il problema della sicurezza: posso davvero fidarmi del client? Con i JSON Web Token (JWT – specifica ancora in draft) si risolve il problema realizzando dei token che in realtà sono degli oggetti JSON, contenente quindi i dati della sessione utente, serializzati in base64 e firmati dal server. Con JWT, il server può fidarsi del token che arriva dal client perché è stato lui a firmarlo. Possiamo invece noi fidarci di un sistema di questo tipo? Dietro alla specifica ci sono voci del calibro di Auth0, OAuth Working Group e Microsoft, quindi perché no?
Cos’è JWT?
JSON Web Token, come dice il nome, non è altro che una stringa, un token, contenente informazioni (chiamate claims, ovvero “affermazioni, dichiarazioni”) in formato JSON. Le informazioni sono firmate lato server secondo la specifica JSON Web Signature (JWS), quindi il server è in grado di riconoscere se sono state generate da lui o meno.
Un token JWT può essere:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsInVzZXJJZCI6ImZvbyIsInJvbGVzIjpbImNhbkVkaXQiLCJjYW5EZWxldGUiXX0.L8q8YoFAxyTpTJZIlRJoIoZ2K9r1hupMVWi8rIm0F6g
ovvero composto da
. .
Traducendo il token sul debugger di JWT si ottiene:
- una testata, in formato JSON e codificata in base64, contenente il tipo di algoritmo usato per la firma e il tipo di token. Questi valori sono obbligatori:
{ "alg": "HS256", "typ": "JWT" }
- un corpo, costituito da una serie di claims, codificati in base64, di affermazioni, in formato JSON:
{ "exp": 1234567890, "userId": "foo", "roles": [ "canEdit", "canDelete" ] }
La specifica contempla una serie di claims riconosciuti: exp è uno di questi e indica la data di scadenza di un token. Gli altri due claims sono personali e ci serviranno più avanti per ricreare lo
UserPrincipal
sul server. - una firma, codificata anch’essa in base64, della testata e del corpo. L’algoritmo usato è di tipo HMAC (Hash-Based Message Authentication Code), che ha bisogno di una funzione hash, specificata nella testata del token, e una chiave di cifratura privata che è a sola conoscenza del server.
Come si vede dal debugger JWT, la firma non è valida perché la chiave non è corretta: vi lascio immaginare quale possa essere…
Come l’autenticazione BASIC, è facile decifrare il contenuto del token: nel caso della BASIC però avrete in mano subito l’utente e la passowrd da poter riusare subito, nel caso di JWT invece sarete a conoscenza di informazioni che non potrete modificare a meno di indovinare la chiave privata del server! In più, il toke può contenere tutto quello che serve per ricreare lo stato sul server, essere riconosciuti ed evitare overhead di autenticazioni.
Sicurezza: token vs cookies
Perché usare i token invece che i vecchi e cari cookie? I sostenitori dei token (come quelli di Auth0), hanno grandi argomentazioni a favore, come nel post di Alberto Pose, di cui riassumo i punti salienti:
- Cross-origin Resource Sharing (CORS): cookie e chiamate cross domain non vanno molto d’accordo. In un mondo dove “l’app” è sempre più all’interno del browser e non sul server, chiamate cross domain sono più frequenti e con i token è possibile accedere a servizi protetti da autenticazione.
- Stateless: come già detto, lo stato sta nel token, il server deve solo validarlo e fidarsi delle informazioni in esso contenute. Questo significa meno consumo di memoria, scalabilità e semplicità.
- Disaccoppiamento: il sistema di autenticazione può essere separato da quello applicativo: il token può essere generato ovunque, da un altro sistema, basta che sia valido.
- Cross-site request forgery (CSRF): gli attacchi di questo tipo sono basati sui cookie, gestiti automaticamente dal browser. Delegando l’autenticazione ad un token nell’header, questo deve essere sempre aggiunto programmaticamente, non sarà mail il browser ad aggiungerlo per noi, impedendo di fatto chiamate non volontarie autenticate.
- Performance: sembra che la validazione del token sia più performante del recupero delle informazioni di un sessione utente
JWT per Tomcat
Visto il nuovo amore sbocciato per questo tipo di sistema di autenticazione, vediamo di introdurlo nella vita di tutti i giorni, con i soliti sistemi di tutti i giorni.
Ultimamente sto lavorando ad un progetto basato sullo stack AngularJS + JAX-RS in Tomcat: quello che mi mancava era proprio un sistema di autenticazione che non mettesse in mezzo la sessione (come fa l’autenticazione FORM) e che fosse stateless (come l’autenticazione BASIC, ma più sicura), con un token “parlante”, che potesse avere scadenza se generato da interfaccia web ma senza scadenza se usato da altre applicazioni: è qua che ho scoperto JWT! Ho creato un progetto (che trovate su github) per integrare le potenzialità di JWT in Tomcat: per capire di che si tratta però è necessario conoscere qualche dettaglio in più su Tomcat.
All’interno di un applicativo web, è possibile implementare un sistema di autenticazione tramite un Web Filter, in modo da validare ogni richiesta in base a certe credenziali: Tomcat implementa lo stesso concetto a livello di Container tramite una Valve, ovvero una vera e propria “valvola” di ingresso delle richieste al server. Tomcat implementa una pipeline di valvole, nella quale possiamo intervenire a diversi livelli:
La figura mostra come le pipeline di valvole siano a livello di “Engine”, piuttosto che di “(virtual) Host” o, in modo più stringente, in prossimità del “contesto” applicativo. A seconda quindi di dove registreremo la nostra nuova valvola avremo JWT per tutte le applicazioni, per un singolo host o per una sola applicazione. Perché implementare una valve? Spostare l’autenticazione a livello di server, invece che all’interno dell’applicazione, permette di usare le annotazioni standard di JAAS (Java Authentication and Authorization Service) anche in Tomcat e rendere il nostro applicativo Java EE compliant, indipendente dall’implementazione del sistema di autenticazione scelto.
JWT Setup
I passi da eseguire per abilitare JWT sono:
- procurarsi i jar da inserire tra le librerie di Tomcat
- registrare la nuova valve (e un realm per l’autenticazione)
- aggiungere i security constraints nel web.xml del proprio applicativo
- generare un token al login dell’applicativo
Procurarsi i jar
Si possono percorrere due strade:
- Clonare il progetto da Github ed effettuare una build com Maven: i jar da deployare vengono creati nella cartella target/to-deploy
- Scaricare gli artefatti direttamente da Maven (valve e dipendenze):
Affinché tutto funzioni, i jar ottenuti devono essere copiati tra le librerie di Tomcat, ovvero nella cartella TOMCA_HOME/lib.
Registrare la nuova valve
Una volta copiati i jar, è necessario registrare la valve a scelta tra i file:
- TOMCAT_HOME/conf/server.xml: a livello di Engine o Host
- MyApp/META-INF/context.xml: a livello di Context
dove secret è la password che firmerà il token.
Aggiungere i security constraints
Prima di abilitare i security constraints nel nostro applicativo, dobbiamo definire un Realm per l’autenticazione. Se vogliamo gestire utenti e ruoli su un database, possiamo configurare un JDBCRealm come spiegato su TheJavaGeek.
Una volta registrata la valve e creato il realm “MyAppRealm”, tramite i security constraints nel web.xml si mettono in sicurezza gli url del nostro applicativo.
Per esempio:
api /api/* * admin devop BASIC MyAppRealm
Occhio al tag
BasicAuthenticator
di Tomcat che controlla subito se esiste già una autenticazione attiva (quella della valve in pratica) ed evita soprattutto che venga creata una sessione web. Omettere questo tag o usare un altro sistema di autenticazione, forza il Tomcat a creare una sessione web, che è quello che non vogliamo!
Come genero un token??
Una volta finita la parte di configurazione, manca da capire come generare un token e come fare a rimanere autenticati tra una chiamata e l’altra. JwtTokenValve
si aspetta il token nell’header (per quanto detto in precendenza) di nome “X-Auth“: ogni richiesta dovrà quindi fornire il token su questo header. Ma come fare a generarlo? Intanto è necessario includere nel vostro progetto una libreria che gestisce i token JWT. E’ possibile includere direttamente il progetto che ha già le dipendenze giuste e qualche classe di utilità in più:
it.cosenonjaviste tomcat-jwt-security 1.0.0 provided
ovviamente in scope provided perché già le abbiamo sul server.
Al login, posso quindi generare un token come segue:
JwtTokenBuilder tokenBuilder = JwtTokenBuilder.create("super secret phrase"); String token = tokenBuilder.userId(securityContext.getUserPrincipal().getName()).roles(Arrays.asList("admin", "devop")).expirySecs(1800).build();
che è valido 30 minuti. La passphrase deve essere la stessa della configurazione della valve.
Al momento del login, la chiamata a getUserPrincipal()
restituirà l’utente autenticato dal MyAppRealm: nelle chiamate successive, sarà invece JwtTokenValve
a popolarne il valore in base alle informazioni contenute nel token!
JAX-RS, JAAS e Tomcat
Avendo scelto di implementare la sicurezza a livello di server con una valve, è possibile usare le annotazioni JAAS insieme a quelle di JAX-RS anche in Tomcat (ovvero non in un contesto Java EE).
Per esempio, una classe che espone un servizio di ricerca potrebbe essere annotato così:
@Path("search") @Produces(MediaType.APPLICATION_JSON) @RolesAllowed({Role.ADMIN, Role.OPERATOR}) public class SearchRestService { ... }
Se si è scelto Jersey 2 come implementazione di JAX-RS 2, è necessario registrare questa “feature” (RolesAllowedDynamicFeature.class
) al momento dell’inizializzazione di Jersey:
public class MyAppConfig extends ResourceConfig { public SerializerAppConfig() { ... register(RolesAllowedDynamicFeature.class); } }
Conclusioni
Con i token JWT possiamo dire finalmente addio alla sessione web sul server e lavorare con Tomcat in modo totalmente stateless!! Se avete suggerimenti e miglioramenti fork e pull request sono ben accetti!! Si potrebbe parametrizzare il nome dell’header che contiene il token per esempio e chi più ne ha più ne metta 😉