Im ersten Teil haben wir uns mit dem OAuth 2 Protokoll und dessen Ablauf beschäftigt. In diesem Teil schauen wir uns nun an, was im Hintergrund alles passiert, wenn sich ein Benutzer mit Facebook bei einer ASP.NET Web API Single Page Application (SPA) anmeldet. Wir werden feststellen, dass im Grunde 2 Authentifizierungen stattfinden. Zum einen authentifiziert sich unser JavaScript Client bei unserer Webanwendung, und diese wiederum authentifiziert sich bei Facebook.
Vorbereitung
Als erstes benötigen wir ein neues ASP.NET Projekt in Visual Studio. Dazu verwenden wir die mitgelieferte Vorlage für eine Single Page Application. Als nächstes muss die Anwendung so konfiguriert werden, dass man sich mit Facebook authentifizieren kann. Wie das funktioniert, wird in diesem Blogpost auf asp.net ausführlich erklärt.
Weiters benötigen wir ein Tool, mit dem wir den HTTP Traffic mitlesen können. Ich empfehle dafür Fiddler von Telerik, da es sehr einfach zu bedienen ist und trotzdem sehr viele Funktionen bietet. Für mich ist Fiddler bei der Entwicklung von Webanwendungen, und vor allem von REST Services, nicht mehr wegzudenken. Da die Anmeldeseite von Facebook über eine sichere HTTPS Verbindung aufgerufen wird, muss in Fiddler in der Option das Entschlüsseln von HTTPS Verbindungen aktiviert werden.
Wie wir später sehen werden, kommuniziert unsere Webanwendung im Hintergrund auch direkt mit Facebook. Damit diese Kommunikation mitprotokolliert werden kann, muss in der web.config unserer Webanwendung der Default Proxy auf den von Fiddler verwendeten Port gesetzt werden. Standardmäßig verwendet Fiddler den Port 8888.
<configuration> <system.net> <defaultProxy> <proxy proxyaddress="http://127.0.0.1:8888" /> </defaultProxy> </system.net> </configuration>
Aufruf der Seite
Nachdem Fiddler gestartet und konfiguriert wurde, können wir die Webanwendung starten. Sobald die Seite im Browser aufgerufen wird, wird vom Client (in diesem Fall ist das der JavaScript Code, der im Browser läuft) überprüft, ob der Benutzer angemeldet ist. Dies passiert, indem er eine asynchrone Anfrage an den Server schickt.
http://localhost:20985/api/Account/UserInfo
Die GetUserInfo Methode, die durch diesen Aufruf ausgeführt wird, soll Informationen über den angemeldeten Benutzer liefern. Da wir zu diesem Zeitpunkt aber noch nicht angemeldet sind, sendet der Server eine Antwort mit dem HTTP Status Code 401 Unauthorized und der Information, dass ein Bearer Token für die Authentifizierung verwendet wird (WWW-Authenticate: Bearer). Dem Benutzer wird daher die Anmeldeseite angezeigt. Damit der Client dem Benutzer auch alle konfigurierten externen Anbieter anzeigen kann, fragt er diese durch eine weitere asynchrone Anfrage ab.
http://localhost:20985/api/Account/ExternalLogins?returnUrl=%2F&generateState=true
Der Server liefert eine Liste der Anbieter im JSON Format.
[{ "name":"Facebook", "url":"/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2F&state=nc[...]41", "state":"nc[...]41" }]
Start des Authentifizierungsprozesses
Um den Authentifizierungsvorgang über Facebook zu starten, klicken wir unter „Use another service to login.“ auf Facebook. In Fiddler sehen wir sofort, dass eine Anfrage an die zuvor abgefragt URL des Providers geschickt wurde.
http://localhost:20985/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2F&state=nc[...]41
Aus der URL ist ersichtlich, dass die GetExternalLogin Methode des Account Controllers aufgerufen wird. Mit diesem Aufruf startet der JavaScript Client die Authentifizierung an unserer Webanwendung über Facebook (provider=Facebook). Für die Anforderung des Access Tokens kommt dabei der Implicit Grant Type (response_type=token) zum Einsatz. Der Parameter client_id enthält die eindeutige Kennung des Clients, die in der Startup.Auth.cs konfiguriert wurde, und redirect_uri gibt die Adresse an, auf die uns die Webanwendung nach dem Authentifizierungsvorgang umleiten soll. Der state Parameter enthält eine Nonce, mit der später überprüft wird, ob die Antwort zu der gesendet Anfrage passt.
Schauen wir uns nun den Code an, der durch diese Anfrage am Server ausgeführt wird (Teile, die im Moment nicht relevant sind, wurden für eine bessere Übersichtlichkeit entfernt). Da der User zu diesem Zeitpunkt noch nicht authentifiziert ist (wir wollen uns ja gerade erst anmelden), wird ein für den übergebenen Provider (in unserem Fall Facebook) passendes ChallengeResult zurückgeliefert. Der Response wird dabei von OWIN und der Facebook Komponente für OWIN zusammengebaut.
// GET api/Account/ExternalLogin [OverrideAuthentication] [HostAuthentication(DefaultAuthenticationTypes.ExternalCookie)] [AllowAnonymous] [Route("ExternalLogin", Name = "ExternalLogin")] public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null) { if (error != null) { return Redirect(Url.Content("~/") + "#error=" + Uri.EscapeDataString(error)); } if (!User.Identity.IsAuthenticated) { return new ChallengeResult(provider, this); } // ... }
Mit diesem ChallengeResult startet die Webanwendung die Authentifizierung bei Facebook und macht dazu ein Redirect auf folgende Seite:
https://www.facebook.com/dialog/oauth?response_type=code&client_id=56[...]11&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2Fsignin-facebook&scope=&state=5f[...]nA
Wir sehen sofort, dass die Webanwendung, im Gegensatz zum JavaScript Client, den Authorization Code Grant (response_type=code) verwendet. Die client_id entspricht in diesem Fall der appId, die bei der Konfiguration angegeben wurde.
Zusätzlich wird folgendes Cookie gesetzt:
Set-Cookie: .AspNet.Correlation.Facebook=cQ[...]e8; path=/; HttpOnly
In diesem Cookie wird die über den state Parameter an Facebook übergebene Nonce gespeichert, damit sie später mit der Antwort von Facebook verglichen werden kann.
Ist man nicht ohnehin schon bei Facebook angemeldet, landet man jetzt auf der Login Seite von Facebook. Nachdem man sich dort erfolgreich angemeldet hat, wird man gefragt, ob man dieser App Berechtigungen geben möchte. Damit unsere Webanwendung die ID des Benutzers auslesen darf, muss man der App zumindest die Berechtigung geben, die ohnehin öffentlich zugänglichen Profilinformationen auszulesen.
Wenn man dem zustimmt, wird man von Facebook wieder zurück zu unserer Webanwendung geleitet. Dafür wird die von unserer Webanwendung angegebene redirect_uri verwendet. Zusätzlich übergibt uns Facebook zwei Parameter:
http://localhost:20985/signin-facebook?code=AQ[...]LM&state=5f[...]nA
Der Parameter code enthält den Authorization Code, der von der Webanwendung angefordert wurde. Dieser Code kann nur einmal verwendet werden und wird von unserer Webanwendung genutzt, um einen Access Token von Facebook anzufordern. Der state Parameter muss exakt jenen Wert enthalten, der zuvor an Facebook übergeben und im .AspNet.Correlation.Facebook Cookie gespeichert wurde.
Die URL /signin-facebook ruft eine spezielle Methode auf, die von der Owin Komponente für Facebook zur Verfügung gestellt wird. Darin wird nun ein Request an Facebook gestartet, um einen Access Token anzufordern.
https://graph.facebook.com/oauth/access_token?grant_type=authorization_code&code=AQ[...]LM&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2Fsignin-facebook&client_id=56[...]11&client_secret=a7[...]bf
Der Parameter grant_type=authorization_code gibt dabei an, dass man den Access Token unter Verwendung des Authorization Codes anfordert. Der Code selbst wird dabei über den code Parameter übergeben. client_id und client_secret übergeben die konfigurierten Werte der Facebook App. Zusätzlich muss auch der redirect_uri Parameter vorhanden sein und exakt den gleichen Wert enthalten, wie zuvor beim Request für den Authorization Code.
Der Response Body enthält nun den Access Token und die Gültigkeitsdauer:
access_token=CA[...]Tu&expires=5183999
Mit diesem Access Token werden nun die öffentlichen Profilinformationen abgefragt:
GET https://graph.facebook.com/me?access_token=CA[...]Tu
Die angeforderten Informationen werden im JSON Format zurückgeliefert:
{ "id":"1562485406","name":"Bernd Hirschmann",[...],"username":"bernd.hirschmann" }
Die Authentifizierung über Facebook ist damit abgeschlossen und das .AspNet.Correlation.Facebook Cookie wird gelöscht. Die Webanwendung kennt nun den Facebook Benutzernamen und die ID, aber ob der Benutzer bei unserer Webanwendung registriert ist und ob er überhaupt berechtigt ist, sich anzumelden, ist noch unklar. Daher wird der Benutzer vorübergehend als externer Benutzer angemeldet und die Informationen dazu im .AspNet.ExternalCookie Cookie gespeichert.
Die Webanwendung setzt nun die Authentifizierung des JavaScript Clients fort, in dem ein Redirect an jene Adresse durchgeführt wird, die ursprünglich vom Client aufgerufen wurde.
Set-Cookie: .AspNet.Correlation.Facebook=; expires=Thu, 01-Jan-1970 00:00:00 GMT Set-Cookie: .AspNet.ExternalCookie=GV[...]kA; path=/; HttpOnly<br/> Location: http://localhost:20985/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2F&state=nc[...]41
Benutzer registrieren
Werfen wir erneut einen Blick in die GetExternalLogin Methode um zu sehen, was sich zum vorherigen Aufruf geändert hat (Nicht relevante Teile wurden wieder entfernt).
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null) { // ... ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity); if (externalLogin == null) { return InternalServerError(); } if (externalLogin.LoginProvider != provider) { // ... } IdentityUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey)); bool hasRegistered = user != null; if (hasRegistered) { // ... } else { IEnumerable<Claim> claims = externalLogin.GetClaims(); ClaimsIdentity identity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType); Authentication.SignIn(identity); } return Ok(); }
Der User ist nun über das .AspNet.ExternalCookie Cookie authentifiziert und es werden die externen Benutzerinformationen (ID und Benutzername von Facebook) aus der Identity ausgelesen und in der Variable externalLogin gespeichert. Mit diesen Informationen wird nun in der Datenbank der Webanwendung nach einem existierenden Benutzer gesucht. Da wir die Seite gerade zum ersten Mal besuchen, wird kein bestehender Benutzer gefunden. Da der Benutzer derzeit nur über ein Cookie authentifiziert ist, der Client aber einen Bearer Token erwartet, wird er nun mit diesem Authentication Type angemeldet. Der JavaScript Client hat den Access Token über den Implicit Grant Type angefordert und daher wird dieser als URL Fragment durch ein Redirect an den Client übergeben.
Location: http://localhost:20985/#access_token=ot[...]eA&token_type=bearer&expires_in=1209600&state=nc[...]41
Der Client ließt nun das URL Fragment aus und entfernt es sofort aus der Adressleiste, damit der Access Token nicht sichtbar ist. Für den Client ist jedoch nicht ersichtlich, ob der Benutzer schon registriert ist oder nicht. Daher schickt er wieder eine asynchrone Anfrage an den Server und versucht erneut, Informationen über den aktuellen Benutzer zu bekommen.
GET http://localhost:20985/api/Account/UserInfo
Da der Benutzer zuvor authentifiziert wurde, liefert der Server nun Informationen im JSON Format.
{ "userName":"bernd.hirschmann","hasRegistered":false,"loginProvider":"Facebook" }
Der Client weiß nun, dass der Benutzer noch nicht registriert und derzeit mit seinen Facebook Daten angemeldet ist. Aus diesem Grund wird dem Benutzer ein Formular angezeigt, mit dem er sich einen Benutzernamen für unsere Webanwendung aussuchen kann.
Die Eingabe des Benutzers wird an die RegisterExternal Methode des Account Controllers geschickt. Sofern der eingegebene Benutzer valide ist und noch nicht existiert, wird der Benutzer in der Datenbank angelegt.
Abschluss
War der Registrierungsvorgang erfolgreich, startet der Client erneut eine Authentifizierung.
http://localhost:20985/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A20985%2F&state=6q[...]Q1
Sehen wir uns nun ein letztes Mal die GetExternalLogin Methode an:
public async Task<IHttpActionResult> GetExternalLogin(string provider, string error = null) { // ... IdentityUser user = await UserManager.FindAsync(new UserLoginInfo(externalLogin.LoginProvider, externalLogin.ProviderKey)); bool hasRegistered = user != null; if (hasRegistered) { Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); ClaimsIdentity oAuthIdentity = await UserManager.CreateIdentityAsync(user, OAuthDefaults.AuthenticationType); ClaimsIdentity cookieIdentity = await UserManager.CreateIdentityAsync(user, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = ApplicationOAuthProvider.CreateProperties(user.UserName); Authentication.SignIn(properties, oAuthIdentity, cookieIdentity); } else { // ... } return Ok(); }
Da der Benutzer nun registriert ist, liefert der UserManager ein User Objekt zurück. Der Benutzer wird mit dem ExternalCookie abgemeldet und mit dem Benutzernamen von unserer Webanwendung angemeldet. Der Access Token wird wieder über das URL Fragment an den Client übergeben. Der Client ruft noch einmal die GetUserInfo Methode auf und erhält nun folgendes JSON als Antwort:
{ "userName":"bernd","hasRegistered":true,"loginProvider":null }
Damit ist der Anmeldevorgang erfolgreich abgeschlossen und der Benutzer kann die Anwendung verwenden.
Fazit
Ich war doch ein wenig überrascht, wie viel eigentlich passiert, wenn man sich mit Facebook an einer Seite anmeldet. Anfangs war mir überhaupt nicht klar, wozu so viele Redirects und HTTP Requests notwendig sind. Doch nachdem ich mich etwas mit dem OAuth 2 Protokoll beschäftigt habe und einmal mit Fiddler und Debugger den Ablauf Schritt für Schritt durchgegangen bin, hat sich alles recht schnell geklärt. Ich hoffe, dass es euch jetzt auch klarer ist, wie der Ablauf bei OAuth 2 funktioniert.
Im nächsten Teil werden wir noch ansehen, wie man sich in einer Windows Phone App mit Facebook anmelden kann.