Un aspetto comune alla maggior parte delle Web API è la gestione della sicurezza: le risorse esposte via Web devono essere accessibili e manipolabili soltanto dagli utenti autorizzati. In altre parole, abbiamo bisogno di un meccanismo per gestire l'autenticazione degli utenti e l'autorizzazione all'accesso delle risorse.
Essendo ASP.NET Web API basato su ASP.NET, potremmo essere tentati di adottare gli approcci messi a disposizione da questa tecnologia. Ad esempio, per l'autenticazione potremmo pensare di sfruttare Forms authentication o Windows authentication. Tuttavia questi approcci non sono del tutto adatti nel nostro contesto.
Forms authentication è pensato per applicazioni interattive ed inoltre, basandosi sulla gestione dei cookie, non è pienamente RESTful. Infatti, i principi REST richiedono che ciascuna richiesta HTTP sia stateless, cioè indipendente dalle richieste precedenti. Tra l'altro, se anche l'utilizzo dei cookie fosse ammissibile, potrebbe rappresentare un approccio valido per i client eseguiti all'interno di un browser, ma per i client nativi si aggiungerebbe la complicazione della loro gestione.
Per quanto riguarda invece Windows authentication, dato che è strettamente collegato alle piattaforme Microsoft, rappresenta un approccio non indicato in un ambiente eterogeneo e multipiattaforma come Internet.
Dal momento che le API di tipo RESTful si ispirano all'adozione di HTTP come protocollo centrale, l'approccio più naturale da adottare nell'implementazione di un meccanismo di autenticazione è la HTTP Basic Authentication.
Esso si basa essenzialmente nell'invio di nome utente e password con ogni richiesta HTTP. Dal momento, però, che le credenziali vengono inviate in chiaro, è opportuno che esse viaggino all'interno di un canale sicuro come può essere HTTPS, cioè HTTP over SSL/TLS.
Ma vediamo come supportare in concreto HTTP Basic Authentication facendo riferimento alla Web API del nostro glossario. Quello che vogliamo implementare è un accesso pubblico in lettura ai termini presenti nel glossario ed un accesso riservato per la loro creazione, modifica ed eliminazione.
Consideriamo dapprima il processo di autenticazione. Lo implementeremo creando un Message handler che si occupa di intercettare nella richiesta HTTP dell'intestazione Authorization con le relative credenziali codificate in Base64, come previsto dallo standard.
public class BasicAuthenticationMessageHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Credentials myCredentials = null;
GenericIdentity identity = null;
if ((request.Headers.Authorization != null)) {
myCredentials = ExtractCredentials(request.Headers.Authorization);
if (IsValidUser(myCredentials)) {
identity = new GenericIdentity(myCredentials.UserName, "Basic");
Thread.CurrentPrincipal = new GenericPrincipal(identity, new string[0]);
}
}
return base.SendAsync(request, cancellationToken);
}
...
}
L'override del metodo SendAsync()
verifica dapprima l'esistenza dell'header HTTP Authorization
. Se questo è presente estrae nome utente e password utilizzando una funzione privata ExtractCredentials(). La funzione ritorna un oggetto di tipo Credential
contenente i dati di accesso dell'utente. Se l'autenticazione ha esito positivo, viene creata un'identità generica per l'utente e viene assegnata al thread corrente.
La funzione privata IsValidUser()
implementa il criterio di autenticazione che preferiamo. Se ad esempio volessimo adottare le Membership API potremmo implementare la funzione nel seguente modo:
private bool IsValidUser(Credentials myCredentials)
{
bool result = false;
if ((myCredentials != null)) {
result =Membership.ValidateUser(myCredentials.UserName, myCredentials.Password)
}
return result;
}
Teniamo presente che quello che abbiamo implementato finora è l'autenticazione dell'utente e l'eventuale sua assegnazione al thread corrente. Allo stato attuale, indipendentemente dal fatto che superi o meno l'autenticazione, un utente ha accesso a tutte le risorse gestite dalle nostre Web API.
Possiamo gestire l'autorizzazione all'accesso delle risorse molto semplicemente sfruttando l'attributo Authorize. Nel seguente modo, ad esempio, impediamo l'accesso alle voci del nostro glossario per tutti gli utenti non autenticati:
[Authorize]
public class ItemsController : ApiController
{
...
}
Ma come abbiamo detto all'inizio, il nostro intento è di consentire l'accesso in lettura alle voci del glossario. Vogliamo soltanto controllare l'accesso in scrittura. Possiamo ottenere questo associando l'attributo Authorize direttamente ai metodi coinvolti, senza assegnarlo all'intera classe:
[Authorize]
public HttpResponseMessage PostItem(Item myItem)
{
...
}
[Authorize]
public void PutItem(string id, Item myItem)
{
...
}
[Authorize]
public HttpResponseMessage DeleteItem(string id)
{
...
}
Possiamo in realtà fare di più. Se abbiamo previsto dei ruoli per i nostri utenti e li abbiamo associati all'identità del thread corrente, possiamo impostare l'accesso ad un metodo in base al ruolo dell'utente, come mostrato di seguito:
[Authorize(Roles="admin")]
public HttpResponseMessage PostItem(Item myItem)
{
...
}
[Authorize(Roles="admin, collab")]
public void PutItem(string id, Item myItem)
{
...
}
[Authorize(Roles="admin")]
public HttpResponseMessage DeleteItem(string id)
{
...
}
Questo consente un controllo granulare sulle risorse gestite dalla nostra Web API con un approccio tutto sommato abbastanza semplice.
Tutti i sorgenti sono in allegato.