La sicurezza di dati e informazioni è un aspetto molto importante all‘interno di sistemi e applicazioni. Java fornisce diversi strumenti per implementare strategie e meccanismi di sicurezza. Un aspetto importante da tenere in considerazione è la necessità di attribuire un‘identità a chi richiede l‘accesso al sistema (autenticazione). All‘interno della Java Security Technology, JAAS si occupa anche di questo aspetto, come vedremo nell‘articolo di questo mese.
Introduzione
Dopo aver visto a grandi linee come funziona la security in Java e aver introdotto alcuni importanti elementi della JAAS-API, veniamo adesso a descrivere uno dei due aspetti di cui si occupa JAAS: l’autenticazione. Dopo aver descritto come si svolge questo processo andremo ad analizzare nei dettagli gli elementi che operano in collaborazione tra loro all’interno del framework.
Il processo di autenticazione
Per poter autenticare un utente abbiamo bisogno di un LoginContext (contenuto nel package javax.security.auth.login). Il LoginContext è responsabile del processo di lettura dei dati dalla Configuration e di istanziazione degli specifici LoginModule. È infatti possibile usare più di un LoginModule, permettendo in tal modo di integrare nell’applicazione diversi meccanismi di autenticazione, rafforzandone la sicurezza: ciò è utile, per esempio, quando l’applicazione è costituita da un insieme di sotto-sistemi, per ognuno dei quali è previsto un differente meccanismo di autenticazione. I LoginModule sono inizializzati con un Subject, un CallbackHandler, uno sharedState (Map) per le informazioni sullo stato del processo e un oggetto options (Map) che riporta le opzioni specifiche del LoginModule.
In JAAS il Subject (javax.security.auth.Subject) rappresenta una sorgente di richieste ed è popolato con le identità, o Principal (classe che implementa l’interfaccia java.security.Principal e java.io.Serializable) associate all’utente che ne fa richiesta.
Il LoginContext usa la lista di LoginModule fornita dalla Configuration, alla entry indicata dalla stringa “name” passata nel costruttore. Ecco un modo per istanziare un LoginContext tratto dalla documentazione ufficiale:
import javax.security.auth.login.*; . . . LoginContext lc = new LoginContext(, );
Al posto dei termini tra parentesi angolari dovremmo inserire rispettivamente:
- Una stringa che individua la entry nel file di configurazione. Questa entry contiene una serie di dichiarazioni che descrivono quali LoginModule usare per l’autenticazione e come usarli.
- Un oggetto che implementa l’interfaccia CallbackHandler. Questo oggetto è necessario per permettere al LoginModule di comunicare con l’utente (sia esso una persona fisica o un sistema o servizio esterno) per raccogliere le informazioni di autenticazione (tipicamente username e password) e, nello stesso tempo, garantire l’indipendenza di implementazione.
Qui di seguito riportiamo un esempio di concreta instanziazione:
LoginContext lc = new LoginContext("MyApp", new CustomCallbackHandler());
In questa circostanza al costruttore del LoginContex viene passata la stringa “MyApp” e un CallbackHandler opportunamente customizzato.
Una volta ottenuta una istanza di classe LoginContext lc, possiamo chiamare il metodo login( ) per eseguire il processo di autenticazione:
lc.login();
Il processo di autenticazione si divide in due fasi: la fase di login e quella di commit.
Nella prima fase (login) il metodo login del LoginContext invoca il corrispondente metodo login() di tutti i LoginModule elencati nel file di configurazione (per quella specifica entry) per avviare il processo di autenticazione.
Figura 1 – Diagramma di sequenza del processo di autenticazione (da [6])
Ogni LoginModule userà il CallbackHandler per ottenere le credential dall’utente. Una volta ottenute le credential (per esempio username e password) ciascun LoginModule verificherà la corrispondenza tra le credential fornite e quelle depositate nel repository di credential.
Non esiste una classe definita per le credential all’interno di JAAS. Una qualsiasi classe può rappresentare delle Credential ma è consigliabile che questa implementi le interfacce Refreshable e Destroyable. Dopo aver eseguito questo processo, il metodo login di un LoginModule salva il suo stato di autenticazione come informazione private.
Nella seconda fase (commit), se l’autenticazione complessiva del LoginContext è andata a buon fine (tutti gli opportuni LoginModule hanno ottenuto l’autenticazione), allora viene invocato il metodo commit di ciascun LoginModule.
Se l’autenticazione complessiva e quella del singolo LoginModule si è conclusa con successo, ciascun LoginModule associa al Subject che rappresenta lo user, i Principal (identità autenticate) e le credentials (dati di autenticazione come chiavi crittografiche). A questo punto l’applicazione è in grado di ottenere un Subject dal LoginContext, invocando il metodo getSubject().
La procedura di log-out si svolge invece in un’unica fase. Il LoginContext invoca il metodo logout ed esegue la rimozione dal Subject di Principal o credential, come pure di eventuali informazioni di sessione.
La Configuration
Come già affermato in precedenza, l’autenticazione in Java è eseguita in maniera pluggable.
Questo significa che le applicazioni possono restare indipendenti dalle tecnologie sottostanti. Un amministratore di sistema determina quali tecnologie di autenticazione (LoginModule) usare agendo sulla configurazione dei login. La sorgente delle informazioni di configurazione è un’implementazione della classe astratta javax.security.auth.login.Configuration. L’implementazione di default legge le informazioni di configurazione da un file ASCII. La Configuration specifica quali LoginModule dovrebbero essere usati per una particolare applicazione, e in quale ordine dovrebbero essere invocati.
Per poter leggere la Configuration, al fine di determinare quali LoginModule sono configurati per una data applicazione (“MyApp” nel nostro esempio), il LoginContext fa le seguenti chiamate:
config = Configuration.getConfiguration(); entries = config.getAppConfigurationEntry("MyApp");
Un file di configurazione per default riporta le informazioni secondo una precisa sintassi:
Application1 { ModuleClass Flag ModuleOptions; ModuleClass Flag ModuleOptions; ModuleClass Flag ModuleOptions; }; Application2 { ModuleClass Flag ModuleOptions; ModuleClass Flag ModuleOptions; }; other { ModuleClass Flag ModuleOptions; ModuleClass Flag ModuleOptions; };
Ciascuna entry nella Configuration si presenta con una stringa (p. e.: “Application1”, che rappresenta il nome dell’applicazione) che serve per indicizzare le informazioni di configurazione. All’interno delle parentesi graffe è presente una lista di LoginModule configurati per quella applicazione. Ciascun LoginModule viene specificato usando il fully qualified name della classe. L’autenticazione procede dall’alto verso il basso nell’elenco dei LoginModule all’interno della entry, nell’esatto ordine specificato. Se non esiste una entry corrispondente a quella fornita, viene automaticamente esaminata la entry “other”.
I Flag hanno lo scopo di disciplinare il comportamento globale dell’autenticazione. I valori dei Flag possono essere i seguenti:
- Required. Il LoginModule è necessario per il successo dell’autenticazione. Sia che l’autenticazione abbia successo o fallisca, il processo di autenticazione comunque procede lungo la lista dei LoginModule.
- Requisite. Il LoginModule è necessario per il successo globale dell’autenticazione. In questo caso se l’autenticazione attraverso questo LoginModule fallisce, il controllo ritorna immediatamente all’applicazione. Se ha successo passa all’elemento successivo.
- Sufficient. Il LoginModule non è necessario per il successo globale dell’autenticazione. Se il processo di autenticazione fallisce, il processo globale passa al LoginModule successivo. In caso contrario passa direttamente all’applicazione, senza considerare gli elementi successivi.
- Optional. Il LoginModule non è necessario per il successo del processo di autenticazione. Che il processo abbia successo o fallisca, l’autenticazione procede comunque la sua discesa lungo la lista dei LoginModule.
Il processo di autenticazione complessivo avrà successo solo se tutti i required e requisite LoginModule avranno successo. Nel caso un LoginModule con flag sufficient restituisca un valore positivo (successo), solo i LoginModule required e requisite precedenti (nella lista) necessitano di portare al successo (al fine di considerare positiva la procedura di autenticazione complessiva). Se non è stato configurato nessun LoginModule come required o requisite, affinche’ l’autenticazione complessiva abbia successo, deve avvenire un’autenticazione positiva almeno per un LoginModule con flag sufficient o optional.
Le ModuleOptions costituiscono un insieme di opzioni specifiche del LoginModule, i cui valori sono passati direttamente al sottostante LoginModule. Le opzioni sono definite nello stesso LoginModule, e controllano il suo comportamento.
Il LoginModule
L’interfaccia LoginModule specifica 5 metodi astratti che richiedono implementazione più altri field (sharedState e options). I field sharedState sono usati per condividere informazioni o dati tra LoginModule raggruppati all’interno di una entry (per esempio informazioni sullo stato delle precedenti autenticazioni).
Le options (gia viste nel paragrafo precedente dal punto di vista amministrativo) sono depositate all’interno dei LoginModule usando oggetti di tipo Map, e vengono recuperate attraverso la key. Infatti la sintassi di queste option è di tipo key-value (per esempio debug=”true”).
Per permettere la sua corretta istanziazione da parte di un LoginContext è inoltre necessario aggiungere un costruttore public senza argomenti. In sua assenza un costruttore di default è ereditato dalla classe Object.
void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options);
Il metodo initialize è invocato al fine di inizializzare il LoginModule con informazioni di autenticazione rilevanti.
Questo metodo è chiamato dal LoginContext immediatamente dopo l’instanziazione del LoginModule e prima di ogni altra chiamata agli altri metodi pubblici e può, in aggiunta, esaminare lo sharedState per determinare quale sia lo stato dell’autenticazione di altri LoginModule presenti nella configurazione e controllare le options per ottenere informazioni necessarie a gestire e orientare il comportamento del LoginModule.
boolean login() throws LoginException;
Il metodo login è invocato nella prima fase del processo di autenticazione e dovrebbe realizzare concretamente l’autenticazione. Per esempio potrebbe generare una richiesta di username e password, e quindi tentare di verificare questi dati usando un database.
Se il LoginModule richiede un’interazione con l’utente, questa non dovrebbe essere gestita direttamente ma delegando questo compito al metodo handle del CallbackHandler (passato al metodo initialize) per eseguire questa interazione con l’utente e settare le informazioni recuperate. Questo perche’ esistono molte diverse modalità di comunicazione con l’utente, ed è preferibile che il LoginModule rimanga indipendente da queste.
Inoltre il metodo login serve solo ad eseguire l’autenticazione e quindi salvare i risultati dell’autenticazione e il corrispondente stato di autenticazione e non dovrebbe associare alcun nuovo Principal o credential al Subject salvato. Al risultato e allo stato avrà successivamente accesso il metodo commit o abort.
Se l’autenticazione fallisce, il metodo login non dovrebbe ritentare di eseguire l’autenticazione. Questa è responsabilità dell’applicazione. È preferibile usare delle chiamate ripetute al metodo login del LoginContext (eseguite quindi dall’interno dell’applicazione) che eseguire molteplici tentativi dall’interno del LoginModule usando il metodo login().
boolean commit() throws LoginException;
Quando l’autenticazione complessiva si conclude con successo, la prima fase può dirsi terminata e può iniziare la seconda fase del processo di autenticazione. La seconda fase consiste nell’invocazione del metodo commit(). Questo metodo dovrebbe accedere ai risultati e al corrispondente stato dell’autenticazione salvato dal metodo login.
Se il risultato dell’autenticazione denota che il metodo login ha fallito la procedura di autenticazione, allora il metodo commit dovrebbe rimuovere e distruggere ogni stato corrispondente precedentemente salvato dai LoginModule che hanno avuto successo.
Se il risultato salvato invece denota che il metodo login ha avuto successo, allora dovrebbe avvenire l’accesso alle corrispondenti informazioni sullo stato di autenticazione al fine di creare il Principal e le credential. Tali Principal e credential dovrebbero quindi essere aggiunti al Subject salvato dal metodo initialize.
boolean abort() throws LoginException;
Il metodo abort è invocato per abortire il processo di autenticazione. Questo accade nella seconda fase quando la prima fase fallisce. Ciò significa che il processo di autenticazione complessivo del LoginContext è fallito.
Questo metodo prima accede ai risultati dell’autenticazione del LoginModule e al corrispondente stato dell’autenticazione salvato precedentemente dal metodo login (e possibilmente da commit), e quindi cancella tutta l’informazione (per esempio username e password).
boolean logout() throws LoginException;
Il metodo logout è invocato per compiere il log out del Subject.
Questo metodo rimuove i Principal e le credential associate al Subject durante l’operazione di commit. Questo metodo non dovrebbe toccare quei Principal o credential precedentemente esistenti nel Subject, o quelli aggiunti dagli altri LoginModule.
Il metodo logout dovrebbe restituire true se il logout ha successo, altrimenti sollevare una LoginException.
Il CallbackHandler
CallbackHandler è un’interfaccia (che si trova nel package javax.security.auth.callback) che viene usata dal LoginModule sia per acquisire dati necessari all’autenticazione (username e password) dall’utente, sia per fornire all’utente informazioni (per esempio sullo stato del processo di autenticazione). CallbackHandler ha un unico metodo:
void handle(Callback[] callbacks) throws java.io.IOException, UnsupportedCallbackException;
Il LoginModule passa al metodo handle() del CallbackHandler un array di opportuni Callback (javax.security.auth.callback.Callback).
JAAS offre alcune implementazioni di Callback tra cui NameCallback, che è il Callback appropriato per recuperare lo username e PasswordCallback, che svolge la stessa funzione nei riguardi della password.
Un esempio di implementazione di metodo handle() è il seguente:
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof NameCallback) { // mostra una richiesta di inserimento dello username . . . } else if (callbacks[i] instanceof PasswordCallback) { // mostra una richiesta di inserimento della password . . . } else { throw new UnsupportedCallbackException (callbacks[i], "Unrecognized Callback"); } } }
Come detto, al metodo handle del CallbackHandler viene passato un array di oggetti che implementano l’interfaccia Callback (NameCallback, PasswordCallback, ecc.). Una volta ottenuto l’array è necessario farne una scansione per poter agire su ciascun Callback presente, eseguendo in tal modo l’interazione con l’utente nel modo più appropriato per l’applicazione in esecuzione.
Nel nostro esempio, il CallbackHandler gestisce due tipi di Callback: NameCallback e PasswordCallback sia per visualizzare la richiesta di inserimento che per recuperare rispettivamente username nel primo caso e password nel secondo.
Dopo aver controllato la corrispondenza della classe, il metodo handle() del CallbackHandler deve richiedere allo user il suo username e successivamente la password. Per fare questo non fa altro che stampare la richiesta nello System.err per poi salvare il valore recuperato dall’utente col metodo setName.
A titolo d’esempio mostriamo qui di seguito i passaggi appena descritti (nel caso dello username):
} else if (callbacks[i] instanceof NameCallback) { // richiede allo user l'inserimento dello username NameCallback nc = (NameCallback)callbacks[i]; System.err.print(nc.getPrompt()); System.err.flush(); nc.setName((new BufferedReader (new InputStreamReader(System.in))).readLine());
Analogo processo coinvolge, in un passaggio successivo, la password che viene salvata usando l’apposito setter setPassword.
Il file di configurazione
Il file di configurazione è il file in cui, per default, si trovano le dichiarazioni relative alla configuration. Questo file è costituito da una o più entry, ciascuna delle quali specifica quali tecnologie di sicurezza, che possono trovarsi nel layer sottostante, devono essere utilizzate per eseguire le operazioni di autenticazione.
La struttura del file è la seguente:
{ ; ; };
Qui di seguito riportiamo un esempio di file di configurazione:
MyApp { com.sun.security.auth.module.UnixLoginModule required; com.sun.security.auth.module.Krb5LoginModule optional useTicketCache="true" ticketCache="${user.home}${/}tickets"; };
Questa configurazione specifica che un’applicazione chiamata “MyApp” necessita che l’utente si autentichi prima con lo UnixLoginModule, che infatti ha un flag required. Anche se l’autenticazione attraverso lo UnixLoginModule fallisce, la procedura complessiva di autenticazione va avanti e viene tentata l’autenticazione tramite il Krb5LoginModule (ovvero Kerberos). Questo aiuta a nascondere l’origine del fallimento. Dal momento che Krb5LoginModule ha associato il flag optional, l’autenticazione complessiva ha successo solo se lo UnixLoginModule ha successo.
Il secondo Login module presenta delle option inizializzate con opportuni valori. La prima option assegna un valore “true” alla variabile “useTicketCache”. Questo permette di abilitare l’uso di una cache per quel particolare meccanismo di autenticazione (in questo caso è la cache in cui andranno depositati i ticket di Kerberos). La option “ticketCache” è inizializzata con una stringa che descrive un percorso in cui trovare la cache. L’implementazione di default della configuration può essere cambiata settando il valore della property “login.configuration.provider” nel file di configurazione delle security properties /lib/security/java.security.
Conclusioni
In questo articolo abbiamo trattato il tema dell’autenticazione con JAAS, illustrando le dinamiche del processo di autenticazione e successivamente descrivendo in dettaglio le caratteristiche interne degli elementi che, all’interno del framework, interagiscono al fine di portare a termine il processo di autenticazione. L’architettura di JAAS permette di ottenere l’indipendenza di implementazione dalle tecnologie di sicurezza, facilitando l’eventuale sostituzione e aggiornamento senza affliggere l’applicazione. Nel prossimo articolo concluderemo la nostra esposizione teorica affrontando nei dettagli l’altro aspetto di cui si occupa JAAS: l’autorizzazione.
Riferimenti
[1] Java™ 2 Platform Security Architecture, Sun Microsystem
http://java.sun.com/javase/6/docs/technotes/guides/security/spec/security-spec.doc.html
[2] Rich Helton – Johennie Helton, “Java Security Solutions”, Wiley Publishing, Inc. 2002
[3] Java™ Security Overview, Sun Microsystem
http://java.sun.com/javase/6/docs/technotes/guides/security/overview/jsoverview.html
[4] Java™ Authentication and Authorization Service (JAAS) – Reference Guide – Sun Microsystem
http://java.sun.com/j2se/1.4.2/docs/guide/security/jaas/JAASRefGuide.html
[5] JAAS Authentication Tutorial – Sun Microsystem
http://java.sun.com/j2se/1.4.2/docs/guide/security/jaas/tutorials/GeneralAcnOnly.html
[6] JAAS in Action – Cotè