Dans cet article je vais m’intéresser à la création d’un Middleware d’authentification pour Katana (l’implémentation par Microsoft d’OWIN) en prenant comme exemple un middleware permettant d’authentifier un utilisateur en utilisant la méthode HTTP Basic. Cette méthode d’authentification est décrite dans un précédent article.
Pour créer un Middleware il faut créer une classe héritant de OwinMiddleware. Cette classe est très généraliste et ne dispose que d’une méthode :
public abstract Task Invoke(IOwinContext context);
L’authentification repose souvent sur les mêmes mécanismes, Microsoft a donc introduit la classe AuthenticationMiddleware (qui hérité de OwinMiddleware)
public abstract class AuthenticationMiddleware<TOptions>
: OwinMiddleware where TOptions : AuthenticationOptions
{
protected AuthenticationMiddleware(OwinMiddleware next, TOptions options)
public TOptions Options { get; set; }
public override async Task Invoke(IOwinContext context);
protected abstract AuthenticationHandler<TOptions> CreateHandler();
}
Cette classe n’est pas très intéressante en soi mais elle nous guide dans l’implémentation : il faut créer un AuthenticationHandler. Cette classe est instanciée à chaque requête et contient quatre méthodes principales. Les deux premières sont appelées lors du traitement de la requête, alors que les deux autres sont appelées lors de l’envoi de la réponse.
- AuthenticateCoreAsync
Utilisée pour authentifier l’utilisateur à l’aide du contenu de la requête (header, url, cookies, etc. ). Renvoie un AuthenticationTicket, qui contient une propriété Identity de type ClaimsIdentity, ou null si l’utilisateur n’est pas authentifié. Cette méthode est appelée automatiquement si le Middleware est considéré comme actif. Dans le cas d’une authentification passive, le Middleware est appelé seulement quand nécessaire, généralement par l’application au niveau de l’application à l’aide de l’AuthenticationManager.
- InvokeAsync
Le but de cette méthode est de déterminer si la requête correspond au “retour” d’une authentification (comme cela pourrait être le cas avec OpenID ou OAuth). Si c’est le cas la méthode doit authentifier l’utilisateur (souvent en utilisant un autre middleware tel que CookieAuthenticationMiddleware), le rediriger vers la bonne URL et renvoyer “true” pour indiquer qu’aucun n’autre Middleware ne doit être appelé. Autrement il suffit de renvoyer “false”.
- ApplyResponseGrantAsync
Cette méthode permet d’ajouter ou d’enlever un cookie, un token, ou autre dans la réponse. Par exemple de le cas d’une authentification par cookie, cette méthode permet de l’ajouter suite à l’authentification et l’enlever suite à une déconnexion.
- ApplyResponseChallengeAsync
Cette méthode permet principalement de traiter les réponses avec le code 401 (unauthorized). A ce moment selon la méthode d’authentification, il est possible de modifier la réponse pour y ajouter un challenge. Par exemple pour une authentification HTTP Basic, on peut ajouter le header “WWW-Authenticate”.
On a maintenant toutes les cartes pour créer le Middleware. Commençons par la partie la plus simple
public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
public BasicAuthenticationMiddleware(OwinMiddleware next, BasicAuthenticationOptions options)
: base(next, options)
{
}
protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
{
return new BasicAuthenticationHandler();
}
}
public delegate Task<AuthenticationTicket> ValidateCredentialHandler(
IOwinContext context,
string userName,
string password);
public class BasicAuthenticationOptions : AuthenticationOptions
{
public ValidateCredentialHandler ValidateCredentials { get; set; }
public string Realm { get; set; }
public BasicAuthenticationOptions()
: base("Basic") { }
}
Maintenant il faut créer le Handler :
public class BasicAuthenticationHandler : AuthenticationHandler
{
protected override Task AuthenticateCoreAsync()
{
if (Options.ValidateCredentials == null)
throw new InvalidOperationException("ValidateCredential must be set.");
var authorizationHeaderValue = Request.Headers.Get("Authorization");
AuthenticationHeaderValue authenticationValue;
if (AuthenticationHeaderValue.TryParse(authorizationHeaderValue, out authenticationValue))
{
if (string.Equals(authenticationValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
return ValidateHeader(authenticationValue.Parameter);
}
}
return Task.FromResult((AuthenticationTicket)null);
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
Response.Headers.Append("WWW-Authenticate", "Basic realm=" + (Options.Realm ?? GetRealm()));
}
return Task.FromResult<object>(null);
}
protected virtual Task<AuthenticationTicket> 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();
}
Task<AuthenticationTicket> result = Options.ValidateCredentials(Context, login, password);
if (result == null)
return Task.FromResult((AuthenticationTicket)null);
return result;
}
}
Pour utiliser le Middleware, il faut l’enregister et définir la méthode permettant de vérifier l’utilisateur. Pour rester dans l’air du temps, la méthode choisie utilise ASP.NET Identity :
public void ConfigureAuth(IAppBuilder app)
{
var basicAuthenticationOptions = new BasicAuthenticationOptions();
basicAuthenticationOptions.ValidateCredentials = (context, userName, password) =>
{
var userManager = context.GetUserManager<UserManager<User>>();
if (userManager == null)
return null;
var user = userManager.FindByName(userName);
if (user == null)
return null;
var userId = ((IUser)user).Id;
var result = userManager.CheckPassword(user, password);
if (result)
{
return Task.FromResult(new AuthenticationTicket(userManager.CreateIdentity(user, "Basic"), null));
}
return null;
};
app.Use(typeof(BasicAuthenticationMiddleware), basicAuthenticationOptions);
}
Et voilà :)