Das Passwort, bitte Wie man HTML (nicht) zu Markdown konvertiert

Wie viel Authentifizierung hättens denn gern?

Published on Friday, May 6, 2016 7:00:00 AM UTC in Programming & Security

Wenn man eine Webseite mit eigenen Inhalten entwickelt, dann stellt man irgendwann fest: "Hmmm, vielleicht wäre es ganz günstig, wenn nicht jeder im Internet meine Artikel bearbeiten könnte". Es muss also eine Art von Authentifizierung und Schutz her, und wenn man sich im Kontext von ASP.NET Core mit dem Thema beschäftigt, landet man sehr schnell bei ASP.NET Identity: User Manager, Sign-In Manager, Entity Framework, Default Token Provider... uff!? Zeit, erstmal einen Schritt zurückzugehen und alles nüchtern von oben zu betrachten.

Was möchte ich eigentlich erreichen? Beim Ablösen von BlogEngine.NET durch eine eigene Webseite geht es darum, meine Inhalte zwar allen zugänglich zu machen, das Bearbeiten derselben aber nur einer einzigen Person - mir. Benötige ich OAuth 2? Zwei-Faktor-Authentifizierung? Brauche ich die Möglichkeit zur Registrierung von Benutzern? Eine Datenbank zur Verwaltung von Zugangsdaten? Nein, nein, nein und nochmals nein. Mein Login-Bildschirm soll wortwörtlich so aussehen:

Login screen.png

Jahrelange haben mich Blogs und CMS dazu gezwungen, wieder und wieder denselben Benutzernamen einzugeben; den einzigen, den das System überhaupt kannte. Damit soll nun Schluss sein: die einzig relevante Information ist das Passwort. In der Schnittstelle meiner Implementierung ist es zwar vorgesehen (so viel YAGNI wäre dann doch eher schädlich):

public interface IAuthenticationService
{
    string AuthenticationType { get; }
    bool Authenticate(string userName, string password);
    void ChangePassword(string userName, string oldPassword, string newPassword);
}

... aber die Implementierung macht schon Schluss damit:

public bool Authenticate(string userName, string password)
{
    if (userName != null)
    {
        throw new ArgumentException("User names are currently not supported", nameof(userName));
    }
	
	// ...
}

Wo implementieren?

Es gibt also einen sehr einfachen IAuthenticationService, auf den ich in einem eigenen Post noch eingehen werden. Aber wo baut man den denn ein? Wenn man ein leeres Projekt mit Authentifizierungsunterstützung in Visual Studio erstellt, fallen direkt mal mehr Codezeilen raus, als das Telefonbuch von München Einträge hat - das muss doch einfacher gehen!? Ja natürlich.

Anstatt Identity (das volle Paket) zu konfigurieren, beschränkt man sich schlicht auf Cookie-Authentication. Dazu benötigt man lediglich das Paket Microsoft.AspNet.Authentication.Cookies und einen passenden Eintrag in der Configure-Methode seiner Startup-Klasse:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationScheme = "CustomBlabla",
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    LoginPath = new PathString("/admin/login")
});

Das sehe ich als die Minimalkonfiguration an, die die Interna der Authentifizierung automatisch regelt, insbesondere eben das Speichern und Auslesen von Authentifzierungsdaten aus Cookies und die nötigen Ping-Pong-Spiele mit dem Browser, um zum Login-Dialog umzuleiten. Im Vollausbau kann man hier noch weitere Dinge konfigurieren, etwa wenn bei Zugriffsverweigerung eine bestimmte Seite angezeigt werden soll.

Auf der Login-Seite wird dann lediglich das oben schon angedeutete Formular angezeigt, und der Post-Back sieht dann folgendermaßen aus:

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel loginViewModel)
{
    try
    {
        if (!_authenticationService.Authenticate(null, loginViewModel.Password))
        {
            // ... Passwort falsch
        }

        var identity = new ClaimsIdentity(
            new[]
            {
                new Claim("sub", "Peter Kuhn")
            },
            _authenticationService.AuthenticationType,
            "sub",
            null);

        await HttpContext.Authentication.SignInAsync("CustomBlabla", new ClaimsPrincipal(identity));

        if (loginViewModel.ReturnUrl != null)
        {
		// ... weiter zum eigentlichen Ziel
	}
	
	// ...
}

Im Kern beschränkt es sich darauf, die SignInAsync-Methode des eingebauten Authentication-Managers aufzurufen. Da diese gerne einen ClaimsPrincipal hätte, wird er zuvor aus den wenigen verfügbaren Informationen konstruiert. Ich missbrauche hier den sub-Claim von OpenId Connect als Identifier und Name gleichzeitig, was aber nicht stört. Wichtig ist, hier dasselbe AuthenticationScheme zu verwenden (hier: "CustomBlabla") wie bei der Konfiguration in der Startup-Klasse.

Tatsächlich war es das schon: der Benutzer ist authentifziert und hat Zugriff auf die Teile der Webanwendung, die z.B. per Authorize-Attribut geschützt sind. Alles weitere, etwa Funktionen zum Ändern des Passworts, sind Komfort-Features, die man dann nach und nach dazu implementieren kann. Lediglich der Logout ist vielleicht noch essenziell, um sich ggfs. an fremden oder öffentlichen Rechnern auch wieder explizit abmelden zu können:

public async Task<IActionResult> LogOut()
{
    await HttpContext.Authentication.SignOutAsync("CustomBlabla");
    return RedirectToAction(nameof(Index), "Home");
}

Das war ja einfach.

Tags: ASP.NET Core · ASP.NET Identity · Cookie Authentication