ASP.NET Web API est un Framework permettant de créer facilement des services web (http). La sécurisation de ces services est un besoin courant. Il existe de nombreuses façons de le faire. Dans cet article nous allons mettre en place une authentification HTTP basic (RFC 2617).
Authentification HTTP Basic
Pour s’authentifier il existe trois méthodes :
- Ce que l’on connait par exemple un couple nom d’utilisateur / mot de passe
- Ce que l’on possède par exemple une carte à puce
- Ce que l’on est par exemple la reconnaissance de l’iris
La méthode d’authentification basic utilise un couple nom d’utilisateur / mot de passe et correspond donc au premier type d’authentification. Son fonctionnement est très simple :
Lorsque l’utilisateur tente d’accéder à la ressource sans être authentifié, le serveur renvoie le statut 401 (Unauthorized) et le header suivant :
WWW-Authenticate : Basic realm= »http ://www. sample. com »
Le client comprend qu’il doit s’authentifier avec la méthode Basic et ré-exécute la même requête mais en incluant le header :
Authorization : Basic TWV6aWFudG91OjEyMzQ1Ng==
“ TWV6aWFudG91OjEyMzQ1Ng== ” correspond au nom d’utilisateur suivi de “ : ” puis de son mot de passe, le tout encodé en base 64. La valeur originale est “ Meziantou :123456 ”. Le serveur peut ainsi authentifier le client. Ce header doit être envoyé à chaque requête.
Web API Pipeline
ASP.NET Web API est composé d’un pipeline permettant de traiter les requêtes. Celui-ci est notamment composé d’une série de DelegatingHandler. Ces handlers traitent les requêtes entrantes les uns après les autres, mais également les réponses. Un handlerpeut soit passer la requête au handler suivant, soit renvoyer directement le résultat au handler précédent et ainsi stopper la progression dans le pipeline.
Un handler peut par exemple effectuer les traitements suivants :
- Lire et modifier les headers de la requête
- Ajouter un header à la réponse
- Authentifier l’utilisateur
- Valider une requête avant qu’elle n’arrive au controlleur
- Tracer / Journaliser les requêtes et les réponses
Vous trouverez un schéma plus complet sur le site officiel : http://www.asp.net/posters/web-api/ASP.NET-Web-API-Poster.pdf
Les DelegatingHandler peuvent s’intégrer dans le pipeline à 2 niveaux :
- De manière globale à l’API : ils interceptent tous les messages
- Par route : ils interceptent uniquement les messages destinés à la route à laquelle ils sont associés
Implémentation
Nous allons donc créer un DelegatingHandler qui :
- À l’entrée essaye d’authentifier l’utilisateur si le header est présent
- À la sortie ajoute le header si le statut de la réponse est 401 (Unauthorized)
public abstract class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic Realm=\"{0}\"";
protected BasicAuthMessageHandler()
{
}
protected BasicAuthMessageHandler(HttpConfiguration httpConfiguration)
{
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected virtual string GetRealm(HttpRequestMessage message)
{
return message.RequestUri.Host;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Process request
AuthenticationHeaderValue authValue = request.Headers.Authorization;
if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter) &&
string.Equals(authValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
// Try to authenticate user
IPrincipal principal = ValidateHeader(authValue.Parameter);
if (principal != null)
{
request.GetRequestContext().Principal = principal;
}
}
return base.SendAsync(request, cancellationToken) // Send message to the InnerHandler
.ContinueWith(task =>
{
// Process response
var response = task.Result;
if (response.StatusCode == HttpStatusCode.Unauthorized &&
!response.Headers.Contains(BasicAuthResponseHeader))
{
response.Headers.Add(BasicAuthResponseHeader,
string.Format(BasicAuthResponseHeaderValue, GetRealm(request)));
}
return response;
}, cancellationToken);
}
private IPrincipal ValidateHeader(string authHeader)
{
// Decode the authentication header & split it
var fromBase64String = Convert.FromBase64String(authHeader);
var lp = Encoding.Default.GetString(fromBase64String);
if (string.IsNullOrWhiteSpace(lp))
return null;
string login;
string password;
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
return ValidateUser(login, password);
}
protected abstract IPrincipal ValidateUser(string userName, string password);
}
La méthode ValidateUser est abstraite et doit donc être implémentée selon votre besoin :
public class SampleBasicAuthMessageHandler : BasicAuthMessageHandler
{
protected override IPrincipal ValidateUser(string userName, string password)
{
if (string.Equals(userName, "Meziantou", StringComparison.OrdinalIgnoreCase) && password == "123456")
return new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[0]);
return null;
}
}
Il ne reste plus qu’à déclarer le handler :
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new SampleBasicAuthMessageHandler());
app.UseWebApi(config);
}
L’exemple complet est disponible sur GitHub : Web Api – Basic Authentication.
Note : Avec ce type d’authentification il est très fortement recommandé d’utiliser HTTPS afin de protéger les identifiants qui autrement circulent en clair sur le réseau.