ASP.NET mette a disposizione tre meccanismi di autenticazione:
1) Windows Authentication (che tratteremo in questo articolo) in cui verranno sfruttati utenti e/o gruppi Windows
2) Forms Authentication (che tratteremo in un articolo separato) in cui gli utenti verranno autenticati tramite cookie (persistente o meno) dopo aver fatto login tramite un form HTML completamente customizzabile.
3) Passport Authentication che si appoggia al servizio centralizzato proposto da Microsoft per il single sign-on su internet.
Una volta autenticato (riconosciuto) l’utente dovremmo configurare le autorizzazioni di accesso per le risorse presenti all’interno dell’applicazione. Il meccanismo di authorization è pressochè identico nelle tre modalità di authentication.
Questo articolo quindi fornirà una panoramica sulla sicurezza Windows e .NET per meglio comprendere la Windows Authentication in ASP.NET e poi concentrarsi sui meccanismi di gestione delle autorizzazioni comuni ai 3 metodi di autenticazione.
Windows Security
Il Common Language Runtime (CLR) .NET fornisce un modello di sicurezza indipendente dal sistema operativo sottostante: agisce come ulteriore “ostacolo da superare” per accedere alle risorse del sistema operativo. Questo significa che un’applicazione .NET può utilizzare utenti e ruoli applicativi indipendenti dagli utenti e gruppi definiti nel sistema operativo. In tutti i casi in cui utenti e gruppi Windows ci stanno stretti possiamo quindi ridefinire il meccanismo di autenticazione e di autorizzazione.Procediamo per gradi.
Quando un utente Windows esegue l’operazione di logon riceve un Security Principal (Token in NT 4.0) che contiene il SID (Security Identifier) dell’utente e l’elenco dei gruppi a cui l’utente appartiene. Il SID è un identificativo univoco assegnato dal sistema operativo nel momento in cui viene creato l’utente in Active Directory (o nel SAM in NT 4.0): il SID rappresenta univocamente l’utente. Per i profani è una sorta di Primary Key per accedere al record delle informazioni dell’utente (non me ne vogliano i sistemisti accaniti).
Ad ogni processo lanciato viene assegnato il Security Principal dell’utente che ha fatto logon. Il processo quindi gira nel sistema operativo nel contesto di sicurezza dell’utente che lo sta utilizzando. Questo meccanismo prende anche il nome di Impersonation, cioè il processo impersona (fa finta di essere) l’utente che lo sta utilizzando.
In Windows nessun processo può girare senza un utente e la sicurezza sulla risorse viene impostata per utenti o gruppi. Quando un processo vuole accedere ad una risorsa viene controllato che il Security Principal a lui assegnato (quello dell’utente che lo ha lanciato appunto) possa effettivamente accedere a quella risorsa.
Sappiamo che alla nascita di un processo corrisponde anche la nascita di un thread interno al processo stesso. Di default i thread interni al processo gireranno con il Security Principal del processo di appartenza: quando un thread vuole accedere ad una risorsa verrà controllato che il Security Principal del processo di appartenenza sia in grado di accedere alla risorsa stessa.
Ad un thread può essere assegnato un Security Principal diverso da quello del processo che lo ospita. Da codice quest’assegnazione si fa con ImpersonateLoggedOnUser, ImpersonateNamedPipeClient o ImpersonateSecurityContext.
Questo meccanismo potrebbe essere sufficiente per garantire sicurezza ad un’applicazione semplice che accede a risorse condivise in rete: se all’utente, o al gruppo Windows a cui appartiene, è garantito l’accesso ad una risorsa, anche tutti i processi lanciati dall’utente stesso potranno accedere a tale risorsa.
Il meccanismo però mostra i suoi limiti non appena scriviamo un’applicazione “vera”. Come fare a garantire l’operazione di bonifico ad alcuni utenti piuttosto che altri quando ovviamente eseguire un bonifico non significa semplicemente accedere in scrittura ad un file su disco. E ancora, come fare a negare ai cassieri appena assunti la possibilità di fare bonifici sopra i 5000 Euro ma poter fare versamenti (che probabilmente implicano un accesso alla stessa risorsa) fino ad un importo di 10.000 Euro. E ancora, come fare a visualizzare alcuni “pezzi” di pagina piuttosto che altri (sulla stessa risorsa quindi) ad utenti diversi.
La soluzione più elastica da sempre è stata quella di scriversi tante righe di codice (magari in varie funzioni...che idea ehh ) che sfruttano oggetti, array, tabelle in database, ruoli MTS o COM+ per far fronte a queste comuni esigenze operative.
.NET Security
Con l’arrivo di .NET molte di queste funzioni sono state inserite nel Common Language Runtime utilizzando lo stesso paradigma Windows.
Ogni thread del CLR gira con un suo Security Principal .NET su cui il codice può effettuare controlli d’accesso.
Un Security Principal .NET è composto da un utente (Identity) e una serie di gruppi associati (Ruoli).
Esistono due tipi di Identity:
1) WindowsIdentity: l’utente corrisponde all’utente Windows
2) GenericIdentity: l’utente viene definito dall’applicazione
Un esempio di WindowsIdentity in C#:
using System;
using System.Security.Principal;
public class demo
{
public static void Main()
{
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
}
} .....
Un esempio di costruzione di una GenericIdentity in una pagina ASP.NET
using System.Security.Principal;
public class demo
{
public static void Main()
{
GenericIdentity MyIdentity = new GenericIdentity("Roberto");
Console.WriteLine(MyIdentity.Name);
}
}
…..
Nel primo esempio di codice, l’applicazione recupera la Windows Identity con il metodo GetCurrent, nel secondo esempio, invece, viene costruita da codice una Generic Identity.
Abbiamo più volte detto che un’Identity forma, insieme ai ruoli, un Security Principal, quind di conseguenza avremo un
1) Windows Principal quando l’applicazione utilizza utente e gruppi del sistema operativo Windows
2) Generic Principal quando utenti e gruppi vengono ridefiniti nell’applicazione
Nell’esempio seguente costruiamo un Windows Principal .NET locale (MyWP) partendo dalla Windows Identity fornita dal Sistema Operativo, per poi visuallizare l’utente e controllarne l’appartenenza al gruppo “DevLeap\Administrator”:
using System;
using System.Security.Principal;
public class demo
{
public static void Main()
{
WindowsPrincipal MyWP = new WindowsPrincipal(WindowsIdentity.GetCurrent());
Console.WriteLine(MyWP.Identity.Name);
Console.WriteLine(MyWP.IsInRole("DevLeap\Administrators"));
}
}
Utilizzando un Generic Principal invece dovremmo costruire utente e ruoli da codice, come si vede dall’esempio seguente:
using System;
using System.Threading;
using System.Security.Principal;
public class demo
{
public static void Main()
{
GenericIdentity MyIdentity = new GenericIdentity("Roberto");
String[] MyRoles = {"Publisher", "User", "Writer"};
GenericPrincipal MyGP = new GenericPrincipal(MyIdentity, MyRoles);
Thread.CurrentPrincipal = MyGP;
Console.WriteLine(MyGP.Identity.Name);
Console.WriteLine(MyGP.Identity.IsAuthenticated);
Console.WriteLine(MyGP.IsInRole("Writer"));
}
}
Un ultimo sforzo: potrebbe avere senso utilizzare account Windows associato a ruoli applicativi ? La risposta è sicuramente si.
Possiamo infatti costruire un Generic Principal in cui l’utente sia una Windows Identity, mentre i ruoli gestiti totalmente da codice e quindi non debbano essere legati ai gruppi Windows.
Questo scenario è molto interessante per tutte le applicazioni in cui tutti gli utenti hanno già un account Windows (pensiamo ad una Intranet ad esempio), ma l’applicazione necessita di ruoli applicativi che non hanno niente a che vedere con quelli impostati dall’amministratore di rete per l’accesso alle risorse condivise. Questo consente anche di avere applicazioni diverse nello stesso dominio che sfruttano account Windows esistenti ma che ridefiniscono i ruoli secondo le esigenze, spesso diversissime, di ognuna di esse.
In questo caso il codice necessario è il seguente:
using System;
using System.Threading;
using System.Security.Principal;
public class demo
{
public static void Main()
{
WindowsIdentity MyIdentity = WindowsIdentity.GetCurrent();
String[] MyRoles = {"Publisher", "User", "Writer"};
GenericPrincipal MyGP = new GenericPrincipal(MyIdentity, MyRoles);
Thread.CurrentPrincipal = MyGP;
Console.WriteLine(MyGP.Identity.Name);
Console.WriteLine(MyGP.Identity.IsAuthenticated);
Console.WriteLine(MyGP.IsInRole("Writer"));
}
}
Vedremo come in ASP.NET il Security Principal .NET venga mppato sull’oggetto User semplicificando quindi le operazioni necessarie che abbiamo visto in questa sezione. Il primo esempio di codice si semplifica ad esempio in:
Response.Write(User.Identity.Name)
Ricordiamo comunque che un processo Windows (con codice .NET o meno) devo comunque utilizzare un Security Principal Windows per poter girare e interagire con il sistema operativo. Questo significa che il codice .NET conoscerà e utilizzerà i Security Principal .NET (Windows Principal o Generic Principal) e il processo che ospita tale codice dovra girare con un Security Principal Windows...sempre che il sistema operativo “sotto” sia Windows...(a buon intenditore...poche parole)
ASP.NET Security
La configurazione di ASP.NET determina sia il Security Principal del CLR, in cui gira l’applicazione web, sia il Security Principal del processo ASPNET_WP (per informazioni sul processo ASPNET_WP vedere l’articolo ASP.NET Process Model).
Per raggiungere un’applicazione web le richieste devono passare da Internet Information Server che fornirà all’applicazione ASP il Security Principal Windows con cui girano i vari thread.
IIS può bloccare la richiesta se non autenticata oppure effettuare un blocco per indirizzo IP oppure negare l’accesso agli utenti privi di certificato client valido. In tutti questi casi la richiesta non arriva all’applicazione ASP.NET e quindi nessuna configurazione effettuata a livello applicativo può bypassare queste impostazioni. Inoltre è IIS a decidere se l’autenticazione Http deve effere fatta con Basic Auth, Digest Auth, Windows Integrated Auth o autenticare l’utente tramite il certificato client.
L’articolo si concentra sull’autenticazione ASP.NET basata su Windows, quindi ognuna delle impostazioni di autenticazione IIS precedentemente elencate (tranne ovviamente il blocco per IP o l’utilizzo di un utente non valido da parte del client) è indipendente rispetto a quanto segue. Nel caso invece di Form Authentication (che sarà trattato in un articolo separato) è necessario abilitare l’accesso anonimo in IIS.
Partiamo dalle impostazioni di default di IIS e di ASP.NET.
1) L’impostazione di default di IIS consente l’accesso all’utente anonimo e utilizza l’account IUSR_nomemacchina per impersonare le richieste anonime (ricordiamo che in Windows un processo non può girare se non nel contesto di un account)
2) In ASP.NET, come si può verificare dal file Machine.Config nella sezione processModel, attributo userName, il processo ASPNET_WP/EWP gira con l’account SYSTEM.
Per default quindi IIS recupera la risorsa da disco (la pagina ASPX) con l’account IUSR_nomemacchina, mentre il codice .NET della pagina gira con l’account SYSTEM.
Il codice sequente consente di verificare questa teoria. E’ sufficiente inserirlo in una pagina ASPX di un sito che consente l’accesso anonimo.
Response.Write(User.Identity.Name)
Response.Write(WindowsIdentity.GetCurrent().Name)
La prima riga di codice visualizza l’utente interno a .NET. L’oggetto User consente di accedere alle proprietà Security Principal .NET in un’applicazione ASP.NET
La seconda riga di codice utilizza invece il metodo GetCurrent per accedere alle proprietà del SecurityPrincipal del processo Windows.
Se in IIS blocchiamo l’utente anonimo l’utente viene forzato a fornire delle credenziali Windows valide per bypassare il controllo di IIS.
In questo caso il thread di INetInfo (IIS) girerà nel contesto dell’utente fornito per l’autenticazione, ma contrariamente a quanto si possa pensare (anche in base all’esperienza su ASP 3.0) il thread che fa girare il codice della pagina ASPX sarà sempre SYSTEM; questo vuol dire che l’eventuale accesso alle risorse Windows verrà effettuato da un unico account, SYSTEM, invece che dai vari utenti che si sono autenticati verso IIS.
Il codice dell’esempio precedente è sempre valido per testare gli account CLR e Windows.
Se è necessario far passare le credenziali dell’utente, ormai potremmo dire il Windows Principal, da IIS a ASP.NET dobbiamo abilitare il meccanismo di impersonation nel file Web.config della singola applicazione o nel file machine.config per tutte le applicazioni web che girano sul server in questione.
Se vogliamo fare un’analogia con il passato, questo meccanismo è assimilabile all’impostazione “Trusted for Delegation” di COM+ in Windows 2000 con la quale il componente COM+ girava nel contesto (impersonava) l’utente che lo aveva invocato remotamente invece di un account generico impostato sul package. Se si pensa al processo ASP.NET come il componente COM+ e a IIS come client che lo invoca l’analogia dovrebbe risultare chiara: le credenziali del “chiamante” vengono riutilizzate dal “chiamato”.
I tag XML necessari per abilitare l’impersonation sono i seguenti:
Riprovando a lanciare la pagina di test verificheremo che IIS ha passato il Security Principal (Windows) verso il processo ASP.NET: nel caso IIS lasci passare l’account anomino l’utente utilizzato all’interno del CLR sarà IUSR_nomemacchina, altrimenti l’utente sarà lo stesso utilizzato per fare logon ad IIS.
Un consiglio durante le prove non anonime: se disabilitate la Windows Authentication in IIS e usate solo la Basic Authentication sarà più semplice testare utenti diversi in quanto il browser vi richiederà le credenziali di logon ogni volta. E’ sufficiente chiudere e riaprire il browser per reiniziare con una nuova prova. La seconda alternativa per facilitare i test consente di utilizzare anche la Windows Authentication: è sufficiente scegliere dal menu Tools/Options del browser il tab Security e impostare nel custom level l’opzione “Prompt for user name and password” sotto la voce User Authentication/Logon come indicato nella figura seguente:
Tornando nel Web.Config è sufficiente aggungere due attributi al tag
password=“Pippo” />
Con l’impersonation attivata è semplicissimo effettuare controlli da codice sull’utente che sta utilizzando l’applicazione.
Il codice VB.NET è il seguente:
If User.Identity.Name = “DevLeap\RobertoB” Then
Ovviamente più interessante è il controllo di appartenenza a gruppi Windows, che in ASP.NET vengono giustamente definiti ruoli applicativi
Il codice VB.NET è il seguente:
If User.IsInRole(“nomeruolo”) Then
Dove nomeruolo corrisponde alla stringa dominio\gruppoWindows come ad esempio “DevLeap\Administrators” nel caso del dominio DevLeap gruppo Administrators.
La soluzione di utilizzare un account applicativo ci ricorda COM+: i componenti di un’applicazione COM+ vengono fatti girare con un account costruito appositamente per l’applicazione.: questa metodologia consente di semplificare gli accessi alle risorse. L’idea alla base è più o meno questa: i vari client utilizzano credenziali diverse, i componenti controllano utenti e ruoli dichiarativamente e/o da codice; quando i componenti interagiscono con le risorse, non è più necessario utilizzare le credenziali dei client in quanto tutti i controlli sono già stati effettuati. In questo modo si semplifica notevolmente la gestione degli accessi alle risorse in quanto sarà il solo account applicativo ad effettuare gli accessi.
Non tutte le applicazioni si adattano a questo scenario (molte comunque), ma se questa situazione fa al caso vostro, sappiate che è configurabile anche in ASP.NET.
Proseguiremo nella seconda parte con le configurazioni del file Web.Config, Machine.config, la gestione delle autorizzazioni di accesso alle risorse...Alla prossima....
