Innovation

Externe Authentifizierung mit ASP.NET Identity

Dino Esposito

Dino EspositoVisual Studio 2013 bietet neue ASP.NET MVC 5- und Web Forms-Projektvorlagen zur Erstellung von Anwendungen. Manche Beispielvorlagen enthalten eine optionale Authentifizierungsebene basierend auf ASP.NET Identity. Der für Sie generierte Code enthält zahlreiche Features, ist jedoch nicht immer leicht zu lesen. In diesem Artikel zeige ich Ihnen, wie Sie Authentifizierung über externe OAuth- oder OpenID-basierte Server wie z. B. soziale Netzwerke implementieren können. Außerdem beschreibe ich die nächsten Schritte nach der Authentifizierung, wenn Sie Zugriff auf alle heruntergeladenen Ansprüche haben.

Ich beginne mit einer leeren ASP.NET MVC 5-Anwendung und füge eine minimale Authentifizierungsebene zur Anwendung hinzu. Sie werden sehen, dass das neue ASP.NET Identity-Framework den Authentifizierungsstapel um Komplexität und neue Funktionen erweitert. Gleichzeitig erleichtert es jedoch die Implementierung einer externen Benutzerauthentifizierung, ohne dass Sie dafür neue Pakete und APIs benötigen.

Auslagern des AccountControllers

Seit der allerersten Version von ASP.NET MVC verwendet die Beispielanwendung des Frameworks für neue Benutzer eine unnötig überfrachtete Controller-Klasse zur Verwaltung aller Authentifizierungsaspekte. In meinem Kommentar “A First Look at ASP.NET Identity” (msdn.microsoft.com/magazine/dn605872) vom März 2014 finden Sie eine Übersicht über das ASP.NET Identity-Framework beginnend mit dem Code, den Sie standardmäßig in Visual Studio erhalten. Im aktuellen Artikel werde ich dagegen mit einem leeren ASP.NET MVC 5-Projekt beginnen und die verschiedenen Komponenten zusammensetzen. Dabei werde ich stets das Eine-Verantwortlichkeit-Prinzip beachten (bit.ly/1gGjFtx).

Ich beginne mit einem leeren Projekt und füge ein ASP.NET MVC 5-Gerüst hinzu. Anschließend erstelle ich zwei neue Controller-Klassen: Login­Controller und AccountController. Der erste Controller befasst sich nur mit An- und Abmeldevorgängen, egal ob diese über ein Systemspezifisches, lokales Mitgliedschaftssystem oder über einen externen OAuth-Anbieter wie Twitter erfolgen. Wie der Name bereits sagt, enthält die Klasse AccountController alle sonstigen Funktionen, die hauptsächlich für die Verwaltung der Benutzerkonten verantwortlich sind. Zur Vereinfachung werde ich in diesem Beispiel nur die Funktion zum Registrieren neuer Benutzer implementieren. Abbildung 1 zeigt das Skelett der beiden Controller-Klassen in Visual Studio 2013.

Skeleton of LoginController and AccountController Classes in Visual Studio 2013
Abbildung 1 Skelett der LoginController- und AccountController-Klassen in Visual Studio 2013

Die Benutzeroberfläche besteht aus drei Teilansichten: logged user, local login form und social login form, siehe Abbildung 2.

The Three Partial Views Used in the Authentication Stack
Abbildung 2 Die drei Teilansichten, die im Authentifizierungsstapel verwendet werden

Die IdentityController-Klasse

In Abbildung 1 erben sowohl LoginController als auch AccountController von der ASP.NET MVC 5 Basis-Controllerklasse. Wenn Sie das ASP.NET Identity-Framework verwenden, ist dies jedoch keine optimale Lösung, da hierbei oft duplizierter Code entsteht. Erstellen Sie daher ein temporäres Projekt in Visual Studio 2013 und sehen Sie sich den Standardcode an, der in der allmächtigen und allumspannenden AccountController-Klasse enthalten ist. ASP.NET Identity verlangt, dass Sie die UserManager<TUser>-Klasse und den Speichermechanismus in den Controller integrieren. Außerdem sollten Sie einige Helfermethoden erstellen, um die Anmeldung von Benutzern und deren Weiterleitung zu vereinfachen. Daneben benötigen Sie einige Eigenschaften, um auf das UserManager-Basisobjekt zu verweisen und evtl. ein weiteres Objekt, um eine Kontaktmöglichkeit für die darunter liegende Open Web Interface for .NET (OWIN)-Middleware zu erstellen.

Sie können z. B. eine Zwischenklasse erstellen, die all diese Codebausteine genau einmal enthält. Ich werde diese Klasse IdentityController nennen und wie in Abbildung 3 gezeigt definieren.

Abbildung 3 Eine neue Basisklasse für die ASP.NET Identity-Authentifizierung

using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AuthSocial.ViewModels.Account.Identity; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; namespace AuthSocial.Controllers { public class IdentityController<TUser, TDbContext> : Controller where TUser : IdentityUser where TDbContext : IdentityDbContext, new() { public IdentityController() : this(new UserManager<TUser>( new UserStore<TUser>(new TDbContext()))) { } public IdentityController(UserManager<TUser> userManager) { UserManager = userManager; } protected UserManager<TUser> UserManager { get; set; } protected IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } protected async Task SignInAsync( TUser user, bool isPersistent) { AuthenticationManager.SignOut( defaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); } protected ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); } } }

Der Einsatz von Generika—und der entsprechenden Einschränkungen—zwingt die IdentityController-Klasse, Benutzer auf Basis der IdentityUser-Klasse und einen Speicherkontext auf Basis der IdentityDbContext-Klasse zu verwenden.

Insbesondere IdentityUser ist lediglich eine Basisklasse, um einen Benutzer mit minimalen Eigenschaften zu beschreiben. Sie können mit einfacher Vererbung neue und spezifischere Klassen erstellen. Geben Sie in diesem Fall einfach Ihre eigene Klasse in der Deklaration der von IdentityController abgeleiteten Klassen an. Hier sehen Sie eine Deklaration von LoginController und AccountController basierend auf IdentityController<TUser, TDbContext>:

public class LoginController : IdentityController<IdentityUser, IdentityDbContext> { ... } public class AccountController : IdentityController<IdentityUser, IdentityDbContext> { ... }

Auf diese Weise legen Sie den Grundstein für die Partitionierung der Komplexität von Authentifizierung und Benutzerverwaltung in drei Code-Klassen. Außerdem haben Sie nun eine Klasse für allgemeine Dienste und eine Klasse, die sich nur auf anmeldungsverwandte Aufgaben konzentriert. Zuletzt haben Sie eine separate Klasse für die Verwaltung von Benutzerkonten. Ich werde mit der Konten-Klasse beginnen und zur Vereinfachung nur einen Endpunkt zum Registrieren neuer Benutzer anbieten.

Die AccountController-Klasse

Normalerweise brauchen Sie nur dann eine Controller-Methode, wenn ein Endbenutzer mit einem Teil der Benutzeroberfläche interagiert. Wenn ein Benutzer z. B. auf eine Schaltfläche zum Übermitteln klicken kann, brauchen Sie eine POST-gesteuerte Controller-Methode. Um diese Methode zu definieren, beginnen wir zunächst mit deren Ansichtsmodell. Die Klasse für das Ansichtsmodell ist ein einfaches Data Transfer Object (DTO), das alle ein- und ausgehenden Daten (Zeichenfolgen, Daten, Zahlen, boolesche Werte und Sammlungen) der angezeigten Benutzeroberfläche sammelt. Sie können die folgende Ansichtsmodell-Klasse verwenden, um einen neuen Benutzer zu einem lokalen Mitgliedschaftssystem hinzuzufügen (der Quellcode ist identisch mit der ASP.NET MVC 5-Vorlage in Visual Studio 2013):

public class RegisterViewModel { public string UserName { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }

Sie können diese Klasse mit Datenanmerkungen zu Validierungs- und Anzeigezwecken versehen. Auf einer höheren Designebene ist jedoch nur entscheidend, dass die Klasse eine Eigenschaft für jedes Eingabe-Steuerelement in der Benutzeroberfläche hat. Abbildung 4 zeigt die Implementierung der Controller-Methode, die POST-Anfragen zur Registrierung neuer Benutzer verarbeitet.

Abbildung 4 Registrieren eines neuen Benutzers

public async Task<ActionResult> Register( RegisterViewModel model) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = model.UserName }; var result = await UserManager.CreateAsync( user, model.Password); if (result.Succeeded) { await SignInAsync(user, false); return RedirectToAction("Index", "Home"); } Helpers.AddErrors(ModelState, result); } return View(model); }

Die CreateAsync-Methode in der UserManager verwendet den zugrunde liegenden Speichermechanismus, um einen neuen Eintrag für den angegebenen Benutzer zu erstellen. Die AccountController-Klasse ist außerdem der ideale Ort für weitere Methoden zum Bearbeiten oder Löschen von Benutzerkonten oder zum Ändern und Zurücksetzen von Kennwörtern.

Die LoginController-Klasse

In der LoginController-Klasse implementieren wir Methoden zum An- und Abmelden von Benutzern auf alle für die Anwendung akzeptablen Arten. Analog zur Funktionsweise des AccountControllers verwaltet die SignIn-Methode ein DTO mit Eigenschaften für die Anmeldeinformationen, eine Kennzeichnung für die Persistenz der Authentifizierung und evtl. eine Rückgabe-URL. Im Standardcode wird die Rückgabe-URL im ViewBag verwaltet, Sie können diese jedoch ebenfalls in die Ansichtsmodell-Klasse einfügen:

public class LoginViewModel { public string UserName { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } public string ReturnUrl { get; set; } }

Der Code zur Interaktion mit dem lokalen Mitgliedschaftssystem wird vollständig auf dem Anwendungsserver verwaltet und stammt aus dem Standardprojekt. Hier sehen Sie einen Auszug aus der POST-Implementierung der neuen SignIn-Methode:

var user = await UserManager.FindAsync( model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); }

Der Code zur eigentlichen Anmeldung von Benutzern ist eine Methode namens SignInAsync, stammt aus der Standardimplementierung und wird als geschützte Methode in der IdentityController-Basisklasse umgeschrieben:

protected async Task SignInAsync(TUser user, bool isPersistent) { AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); }

Mit dem ASP.NET Identity Framework verwenden Sie keine Klassen, die direkt mit der ASP.NET-Codebasis verwandt sind. Im Kontext eines ASP.NET MVC-Hosts verwalten Sie die Authentifizierung von Benutzern weiterhin über Anmeldeformulare und ASP.NET-Authentifizierungs-Cookies, allerdings verwenden Sie die ASP.NET-Klasse FormsAuthentication nicht direkt. Herkömmliche ASP.NET-Entwickler müssen sich mit zwei Codeebenen auseinandersetzen. Eine dieser Ebenen ist die ASP.NET Identity API in Form von Klassen wie z. B. UserManager und IdentityUser. Die zweite Ebene ist die OWIN Cookie-Middleware, welche die Fassade der Authentifizierungs-API von den genauen Details der Speicherung und Persistierung der Benutzerinformationen abschirmt. Sie verwenden weiterhin Formular-Authentifizierung, und das gute alte .ASPXAUTH-Cookie wird weiterhin generiert. All dies geschieht nun jedoch hinter den Kulissen einiger neuer Methoden. Genauere Informationen hierzu finden Sie im Blogeintrag "A Primer on OWIN Cookie Authentication Middleware for the ASP.NET Developer" von Brock Allen unter bit.ly/1fKG0G9.

Sehen Sie sich den folgenden Code an, der direkt in Startup­Auth.cs aufgerufen wird, dem Einstiegspunkt der OWIN-Middleware:

app.UseCookieAuthentication( new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login/SignIn") }); app.UseExternalSignInCookie( DefaultAuthenticationTypes.ExternalCookie); app.UseTwitterAuthentication( consumerKey: "45c6...iQ", consumerSecret: "pRcoXT...kdnU");

Dieser Code ist eine Art fließende Schnittstelle, die Sie anstelle der herkömmlichen web.config-Authentifizierungseinträge verwenden. Außerdem verknüpft der Code die Twitter-basierte Authentifizierung und konfiguriert die OWIN-Middleware zur Verwendung von ExternalSignInCookie, um Informationen über Benutzeranmeldungen durch externe Anmeldungsanbieter temporär zu speichern.

Das AuthenticationManager-Objekt ist der Einstiegspunkt der OWIN und die Fassade, über die Sie die zugrunde liegende ASP.NET-Authentifizierungs-API basierend auf FormsAuthentication aufrufen.

Auf diese Weise erhalten Sie eine einheitliche API, mit der Sie Benutzer authentifizieren und Anmeldeinformationen prüfen können, egal ob das Mitgliedschaftssystem lokal auf Ihrem Webserver oder extern bei einem sozialen Netzwerk liegt. Mit dem folgenden Code können sich Benutzer mit ihren Twitter-Konten bei Ihrer Website anmelden:

[AllowAnonymous] public ActionResult Twitter(String returnUrl) { var provider = "Twitter"; return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Login", new { ReturnUrl = returnUrl })); }

Zuallererst müssen Sie dazu eine Twitter-Anwendung konfigurieren und zu einem Twitter-Entwicklerkonto zuordnen. Weitere Informationen finden Sie unter dev.twitter.com. Eine Twitter-Anwendung ist eindeutig durch zwei alphanumerische Zeichenfolgen gekennzeichnet, die als Consumer Key und Secret bezeichnet werden.

Der Standardcode aus dem ASP.NET MVC 5-Projektassistenten verwendet außerdem eine komplett neue Klasse—ChallengeResult—zur Verarbeitung externer Anmeldungen, wie in Abbildung 5 gezeigt.

Abbildung 5 Die ChallengeResult-Klasse

public class ChallengeResult : HttpUnauthorizedResult { public ChallengeResult(string provider, string redirectUri) { LoginProvider = provider; RedirectUri = redirectUri; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; context.HttpContext .GetOwinContext() .Authentication .Challenge(properties, LoginProvider); } }

Diese Klasse verwendet eine Methode namens Challenge, um den Verbindungsaufbau zum entsprechenden Anmeldungsanbieter (beim Anwendungsstart registriert) für die Authentifizierung an die OWIN-Middleware zu delegieren. Die Rückgabe-URL ist die Methode ExternalLoginCallback im LoginController:

[AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback( string returnUrl) { var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) return RedirectToAction("SignIn"); var identity = GetBasicUserIdentity(info.DefaultUserName); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true }, identity); return RedirectToLocal(returnUrl); }

Die Methode GetExternalLoginInfoAsync ruft Informationen über den identifizierten Benutzer vom entsprechenden Anmeldungsanbieter ab. Nach Abschluss dieser Methode hat die Anwendung genügend Informationen, um den Benutzer anzumelden, aber es ist noch nichts geschehen. An dieser Stelle ist es entscheidend, dass Sie sich Ihrer Optionen bewusst sind. Sie könnten nun einfach fortfahren und einen kanonischen ASP.NET-Authentifizierungs-Cookie erstellen, wie in meinem Beispiel gezeigt. Die Helfermethode GetBasicUserIdentity in der Klasse IdentityController erstellt ein ClaimsIdentity-Objekt für den angegebenen Twitter-Kontonamen:

protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim( ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); }

Sobald der Twitter-Benutzer der Twitter-Anwendung die Informationsweitergabe genehmigt, empfängt die Webseite den Benutzernamen und möglicherweise weitere Daten, wie in Abbildung 6 gezeigt (die Details unterscheiden sich je nach verwendeter externer API).

The Classic Twitter-Based Authentication
Abbildungen 6 Die klassische Twitter-basierte Authentifizierung

Beachten Sie, dass in der ExternalLoginCallback-Methode im gezeigten Code kein Benutzer zum lokalen Mitgliedschaftssystem hinzugefügt wird. Dies ist das einfachste Szenario. In anderen Fällen kann es sein, dass Sie mit den Twitter-Informationen programmgesteuert einen Link zwischen lokaler Anmeldung und dem extern authentifizierten Benutzernamen registrieren möchten (diese Lösung wird im Standard-Authentifizierungscode aus dem Assistenten präsentiert). Außerdem können Sie Benutzer auf eine andere Seite weiterleiten, auf der sie eine E-Mail-Adresse eingeben oder sich für Ihre Website registrieren können.

Mischung aus herkömmlicher und sozialer Authentifizierung

Beinahe jede Website benötigt ein Minimum an Authentifizierung. ASP.NET Identity vereinfacht den gemischten Einsatz von herkömmlicher und sozialer Authentifizierung hinter einer einheitlichen Fassade. Sie benötigen jedoch eine klare Strategie für die Verarbeitung Ihrer Benutzerkonten und für die Daten, die Sie über Ihre Benutzer sammeln und speichern möchten. Eine Mischung aus herkömmlicher und sozialer Authentifizierung bedeutet, dass Sie einzelne Benutzerkonten zu verschiedenen Anmeldungsanbietern wie Facebook, Twitter und sämtlichen anderen von ASP.NET Identity unterstützten Anbietern zuordnen können. Auf diese Weise können sich Ihre Benutzer auf verschiedene Arten anmelden, sind jedoch aus der Sicht Ihrer Anwendung dennoch einzigartig. Bei Bedarf können weitere Anbieter hinzugefügt oder entfernt werden.

Dino Esposito ist der Autor von „Architecting Mobile Solutions for the Enterprise” (Microsoft Press, 2012) sowie des in Kürze erscheinenden „Programming ASP.NET MVC 5” (Microsoft Press). Esposito ist Technical Evangelist für die .NET Framework- und Android-Plattformen bei JetBrains und spricht häufig auf Branchenveranstaltungen weltweit. Auf software2cents.wordpress.com und auf Twitter unter twitter.com/despos lässt er uns wissen, welche Softwarevision er verfolgt.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Pranav Rastogi