Benutzerdefinierte OWIN-Auth-Middleware in ASP.NET mit Katana/OWIN (Update)

Update vom 1. 9. 2013: Das Listing und die Beschreibung wurden an die letzten API-Änderungen in der aktuelle RC angepasst.

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 AuthenticateCoreAsync 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 AuthenticationProperties. Konnte AuthenticateCoreAsync 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 ApplyResponseGrantAsync 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.

ApplyResponseChallengeAsync 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 ApplyResponseChallengeAsync 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
{

    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;
    }


    public override Task InvokeAsync()
    {
        // Standard: false --> Weitermachen ...
        return base.InvokeAsync();
    }


        

    protected override Task AuthenticateCoreAsync()
    {

        var emptyTicket = new AuthenticationTicket(null, new AuthenticationProperties());


        var header = this.Request.Headers["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 AuthenticationProperties());

        return Task.FromResult(ticket);
    }

    protected override Task ApplyResponseGrantAsync()
    {
        return base.ApplyResponseGrantAsync();
    }



    protected override async Task ApplyResponseChallengeAsync()
    {
        if (this.Response.StatusCode == 401)
        {
            Response.Headers.Add("WWW-Authenticate", new [] { "Basic" });
        }


    }
}


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);
    }
}

 

[1] http://katanaproject.codeplex.com/documentation
[2] http://owin.org/
[3] http://weblogs.asp.net/pglavich/archive/2013/04/05/owin-katana-and-getting-started.aspx
[4] http://odetocode.com/blogs/scott/archive/2013/07/09/getting-started-with-owin-katana-and-vs2013.aspx

 

Schulung und Beratung

Modern Web mit Angular 2

Datenbindung, Formulare, Validierung, Routing, HTTP, Komponenten, ...

Details

Migration auf Angular 2

Bestehende Projekte auf Angular 2 migrieren, ngUpgrade, ...

Details

Progressive Web-Apps mit Angular 2

InHouse-Schulung und/oder Beratung maßgeschneidert für Ihre Lernziele

Details

Architektur-Workshop

Interaktiver Prototyp-Workshop für Ihre Anwendung

Details

Entity Framework (EF)

Datenzugriff mit Entity Framework, Mapping-Szenarien, CRUD, Transaktionen, Migrations, Stored Procedures, Vererbung, Neuerungen in Version 7

Details

Angular 2: Deep Dive

Erweiterte Aspekte von Angular 2

Details

ASP.NET WebAPI

Web APIs mit ASP.NET, HTTP, REST, Security, Formatter, Tracing, OData, Streaming

Details

Web APIs mit ASP.NET MVC 6

Web APIs mit ASP.NET, HTTP, REST, Security, Formatter, Tracing, OData, Streaming

Details

Moderne Security-Szenarien für Web APIs

OAuth 2, OpenId Connect, JWT, Spielarten und Flows, ...

Details

Weitere Schulungen ...