In der nächsten Version von ASP.NET, welche mit Visual Studio 2013 ausgeliefert wird, basieren die Scecurity-Mechanismen auf Katana [1], der freien OWIN-Implementierung von Microsoft [2]. Dies gibt dem Entwickler die Möglichkeit, dieselben Komponenten sowohl beim klassischen Hosting innerhalb von IIS als auch in Self-Hosting-Szenarien sowie unabhängig vom verwendeten Framework, wie zum Beispiel ASP.NET Web Forms, ASP.NET MVC oder ASP.NET Web API zu verwenden. Dazu wurde bereits viel geschrieben, zum Beispiel unter [3] und [4].
Zur Implementierung eigener Security-Komponenten bietet Katana die Basis-Klasse AuthenticationMiddleware an. Die nachfolgenden Listings demonstrieren, wie damit das Authentifizierungs-Verfahren HTTP BASIC implementiert werden kann.
Die Klasse HttpBasicAuthenticationOptions erbt von AuthenticationOptions und delegiert an den Konstruktor dieser Super-Klasse die Bezeichnung des implementierten Authentication-Typs weiter. Dabei handelt es sich um eine Zeichenkette, welche die jeweilige Authentifizierungs-Art beschreibt. Standardmäßig verwendet das hier betrachtete Beispiel den String Basic.
Daneben bekommen Derivate von AuthenticationOptions weitere Eigenschaften, mit welchen der Entwickler das Authentifizierungsverfahren parametrisieren kann, spendiert. Hier beschränkt sich dies auf die Eigenschaft ValidateCredentials, welche auf eine Action verweist, die Benutzername und Passwort entgegennimmt und true retourniert, wenn diese beiden Angaben korrekt sind; ansonsten hat sie false zu liefern.
Der HttpBasicAuthenticationHandler ist der Dreh- und Angelpunkt der hier diskutierten Implementierung. Sie erbt von der von Katana bereitgestellten Klasse AuthenticationHandler und fixiert ihren Typparameter auf den Typ der zu verwendenden Options-Klasse. Dabei handelt es sich hier um die zuvor besprochene Klasse HttpBasicAuthenticationOptions.
Die Authentifizierungslogik findet in der zu überschreibenden Methode AuthenticateCore statt. Diese wird - sofern die AuthenticationMiddleware im aktiven Modus eingesetzt wird - bei jedem Seitenaufruf ausgeführt. Konnte diese Methode den aktuellen Benutzer Authentifizieren, liefert sie ein AuthenticationTicket mit Informationen über diesen Benutzer retour. Bei diesen Informationen handelt es sich um ein IIdentity-Objekt sowie um ein Objekt vom Typ AuthenticationExtra. Konnte AuthenticateCore den Benutzer nicht authentifizieren, hat sie ein AuthenticationTicket, welches den Wert null für die IIdentity aufweist, zurückzuliefern.
Wird die AuthenticationMiddleware im passiven Modus eingesetzt, muss die Applikation bei Bedarf die Authentifizierung anfordern. Diesem Fall wird in diesem Beitrag jedoch nicht weiter Beachtung geschenkt.
Die überschriebene Methode ApplyResponseGrant wird ausgeführt, nachdem der Benutzer erfolgreich authentifiziert wurde. Hier könnte der Entwickler ein Session-Cookie setzen. Davon wird hier abgesehen. Stattdessen kommt die Standardimplementierung der Basis-Klasse zum Einsatz.
ApplyResponseChallenge wird ausgeführt, um der HTTP-basierten Antwort Informationen über mögliche Authentifizierungs-Arten hinzuzufügen. Die hier gezeigte Implementierung prüft, ob dem Aufrufer der Zugriff auf die angeforderte Ressource (Seite) mangels Berechtigungen verweigert wurde. Dies wird durch den Status-Code 401 angezeigt. Ist dem so, weist ApplyResponseChallenge den Aufrufer unter Verwendung des Headers WWW-Authenticate darauf hin, dass er sich mittels HTTP BASIC bei der Web-Anwendung anmelden kann, um ggf. die nötigen Rechte zu erhalten.
Die Klasse HttpBasicAuthenticationMiddleware, welche von AuthenticationMiddleware erbt, liefert eine Instanz des zuvor betrachteten Handlers über ihre Methode CreateHandler retour. Indem diese Klasse als OWIN-Middleware registriert wird, bekommt Katana Zugriff auf den Handler. Dies geschieht per Konvention innerhalb der Methode Configuration der Klasse Startup. Diese Methode delegiert im betrachteten Beispiel an RegisterHttpBasicAuthMiddleware weiter. RegisterHttpBasicAuthMiddleware erzeugt das Options-Objekt, setzt den gewünschten Modus (Active oder Passive) sowie die oben beschriebene Eigenschaft ValidateCredentials. Anschließend registriert sie den Typ von HttpBasicAuthenticationMiddleware bei Katana und gibt an, dass dessen Instanzen mit dem Options-Objekt zu parametrisieren sind.
Um diese Implementierung zu testen, muss man nun nur mehr auf eine Ressource zugreifen, die nur für authentifizierte Benutzer zur Verfügung steht. Geschieht dies über einen Browser, wird dieser den Benutzer auffordern, Benutzername und Passwort zu erfassen, nachdem er den Statuscode 401 erhalten hat. Anschließend wird der diese Informationen per HTTP BASIC an die angeforderte Ressource senden. Greift der Entwickler hingegen auf einen Service programmatisch zu, muss er einen entsprechenden Authorization-Header angeben, beispielsweise
Authorization: Basic bWF4OmdlaGVpbQ==
um den Benutzernamen max mit dem Passwort geheim zu verwenden.
public class HttpBasicAuthenticationOptions : AuthenticationOptions
{
public HttpBasicAuthenticationOptions(string authenticationType) : base(authenticationType) { }
public HttpBasicAuthenticationOptions(): base("BASIC") { }
public Func<string, string, bool> ValidateCredentials { get; set; }
}
public class HttpBasicAuthenticationHandler : AuthenticationHandler<HttpBasicAuthenticationOptions>
{
protected override Task<AuthenticationTicket> AuthenticateCore()
{
var emptyTicket = new AuthenticationTicket(null, new AuthenticationExtra());
var header = this.Request.GetHeader("Authorization");
if (string.IsNullOrEmpty(header) ||
!header.Trim().ToLower().StartsWith("basic"))
{
return Task.FromResult(emptyTicket);
}
header = header.Trim();
header = header.Substring(5); // Basic wegschneiden ...
header = header.Trim();
header = DecodeBase64(header);
var index = header.IndexOf(:);
if (index == -1)
{
return Task.FromResult(emptyTicket);
}
var user = header.Substring(0, index);
var password = header.Substring(index + 1);
if (Options.ValidateCredentials != null)
{
if (!Options.ValidateCredentials(user, password))
{
return Task.FromResult(emptyTicket);
}
}
var identity = new ClaimsIdentity(Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, user));
// Weitere Claims ermitteln und setzen ...
identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
var ticket = new AuthenticationTicket(identity, new AuthenticationExtra());
return Task.FromResult(ticket);
}
protected override Task ApplyResponseGrant()
{
return base.ApplyResponseGrant();
}
protected override async Task ApplyResponseChallenge()
{
if (this.Response.StatusCode == 401)
{
Response.AddHeader("WWW-Authenticate", "Basic");
}
}
private static string DecodeBase64(string header)
{
header = Encoding.UTF8.GetString(Convert.FromBase64String(header));
return header;
}
private static string RemovePrefix(string str, string prefix)
{
if (str.StartsWith(prefix))
{
str = str.Substring(prefix.Length, str.Length - prefix.Length);
}
return str;
}
}
class HttpBasicAuthenticationMiddleware : AuthenticationMiddleware<HttpBasicAuthenticationOptions>
{
public HttpBasicAuthenticationMiddleware(OwinMiddleware next, HttpBasicAuthenticationOptions options) : base(next, options)
{
}
protected override AuthenticationHandler<HttpBasicAuthenticationOptions> CreateHandler()
{
return new HttpBasicAuthenticationHandler();
}
}
public class Startup
{
public void Configuration(IAppBuilder app)
{
[...]
RegisterHttpBasicAuthMiddleware(app, AuthenticationMode.Active);
[...]
}
private static void RegisterHttpBasicAuthMiddleware(IAppBuilder app, AuthenticationMode mode)
{
var options = new HttpBasicAuthenticationOptions();
options.AuthenticationMode = mode;
options.ValidateCredentials = (user, pwd) =>
{
if (user == "max" && pwd == "geheim") return true;
return false;
};
app.UseType(typeof (HttpBasicAuthenticationMiddleware), options);
}
}