Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2 (Schützen einer Web-API mit einzelnen Konten und lokaler Anmeldung in ASP.NET-Web-API 2.2)

von Mike Wasson

Beispiel-App herunterladen

In diesem Thema wird gezeigt, wie Sie eine Web-API mithilfe von OAuth2 schützen, um sich bei einer Mitgliedschaftsdatenbank zu authentifizieren.

Im Tutorial verwendete Softwareversionen

In Visual Studio 2013 bietet Ihnen die Web-API-Projektvorlage drei Optionen für die Authentifizierung:

  • Einzelne Konten: Die App verwendet eine Mitgliedschaftsdatenbank.
  • Organisationskonten. Benutzer melden sich mit ihren Azure Active Directory-, Office 365- oder lokalen Active Directory-Anmeldeinformationen an.
  • Windows-Authentifizierung. Diese Option ist für Intranetanwendungen vorgesehen und verwendet das IIS-Modul Windows-Authentifizierung.

Weitere Informationen zu diesen Optionen finden Sie unter Erstellen ASP.NET Webprojekte in Visual Studio 2013.

Einzelne Konten bieten zwei Möglichkeiten für einen Benutzer, sich anzumelden:

  • Lokale Anmeldung. Der Benutzer registriert sich auf der Website und gibt einen Benutzernamen und ein Kennwort ein. Die App speichert den Kennworthash in der Mitgliedschaftsdatenbank. Wenn sich der Benutzer anmeldet, überprüft das ASP.NET Identity-System das Kennwort.
  • Anmeldung für soziale Netzwerke Der Benutzer meldet sich mit einem externen Dienst wie Facebook, Microsoft oder Google an. Die App erstellt weiterhin einen Eintrag für den Benutzer in der Mitgliedschaftsdatenbank, speichert jedoch keine Anmeldeinformationen. Der Benutzer authentifiziert sich, indem er sich beim externen Dienst anmeldet.

In diesem Artikel wird das szenario für die lokale Anmeldung behandelt. Sowohl für die lokale Anmeldung als auch für soziale Netzwerke verwendet die Web-API OAuth2, um Anforderungen zu authentifizieren. Die Anmeldeinformationsflüsse unterscheiden sich jedoch für die lokale Anmeldung und die Anmeldung in sozialen Netzwerken.

In diesem Artikel veranschaulichen wir eine einfache App, mit der sich der Benutzer anmelden und authentifizierte AJAX-Aufrufe an eine Web-API senden kann. Sie können den Beispielcode hier herunterladen. In der Infodatei wird beschrieben, wie Sie das Beispiel in Visual Studio von Grund auf neu erstellen.

Abbildung des Beispielformulars

Die Beispiel-App verwendet Knockout.js für die Datenbindung und jQuery zum Senden von AJAX-Anforderungen. Ich konzentriere mich auf die AJAX-Aufrufe, sodass Sie nicht wissen müssen, Knockout.js für diesen Artikel.

Unterwegs beschreibe ich Folgendes:

  • Was die App auf der Clientseite ausführt.
  • Was geschieht auf dem Server?
  • Der HTTP-Datenverkehr in der Mitte.

Zunächst müssen wir eine OAuth2-Terminologie definieren.

  • Ressource: Einige Daten, die geschützt werden können.
  • Ressourcenserver. Der Server, der die Ressource hostet.
  • Ressourcenbesitzer. Die Entität, die die Berechtigung für den Zugriff auf eine Ressource erteilen kann. (In der Regel der Benutzer.)
  • Client: Die App, die Zugriff auf die Ressource haben möchte. In diesem Artikel ist der Client ein Webbrowser.
  • Zugriffstoken. Ein Token, das Zugriff auf eine Ressource gewährt.
  • Bearertoken. Ein bestimmter Typ von Zugriffstoken mit der Eigenschaft, dass jeder das Token verwenden kann. Anders ausgedrückt: Ein Client benötigt keinen kryptografischen Schlüssel oder ein anderes Geheimnis, um ein Bearertoken zu verwenden. Aus diesem Grund sollten Bearertoken nur über HTTPS verwendet werden und relativ kurze Ablaufzeiten aufweisen.
  • Autorisierungsserver. Ein Server, der Zugriffstoken ausgibt.

Eine Anwendung kann sowohl als Autorisierungsserver als auch als Ressourcenserver fungieren. Die Web-API-Projektvorlage folgt diesem Muster.

Lokaler Anmeldeinformationsfluss

Für die lokale Anmeldung verwendet die Web-API den in OAuth2 definierten Kennwortfluss des Ressourcenbesitzers .

  1. Der Benutzer gibt einen Namen und ein Kennwort in den Client ein.
  2. Der Client sendet diese Anmeldeinformationen an den Autorisierungsserver.
  3. Der Autorisierungsserver authentifiziert die Anmeldeinformationen und gibt ein Zugriffstoken zurück.
  4. Um auf eine geschützte Ressource zuzugreifen, schließt der Client das Zugriffstoken in den Autorisierungsheader der HTTP-Anforderung ein.

Diagramm des lokalen Anmeldeinformationsflusses

Wenn Sie in der Web-API-Projektvorlage Einzelne Konten auswählen, enthält das Projekt einen Autorisierungsserver, der Benutzeranmeldeinformationen überprüft und Token ausgibt. Das folgende Diagramm zeigt denselben Anmeldeinformationsfluss in Bezug auf Web-API-Komponenten.

Diagramm, bei dem einzelne Konten im Web A P I ausgewählt sind

In diesem Szenario fungieren Web-API-Controller als Ressourcenserver. Ein Authentifizierungsfilter überprüft Zugriffstoken, und das Attribut [Authorize] wird verwendet, um eine Ressource zu schützen. Wenn ein Controller oder eine Aktion über das Attribut [Authorize] verfügt, müssen alle Anforderungen an diesen Controller oder diese Aktion authentifiziert werden. Andernfalls wird die Autorisierung verweigert, und die Web-API gibt den Fehler 401 (Nicht autorisiert) zurück.

Der Autorisierungsserver und der Authentifizierungsfilter rufen beide eine OWIN-Middlewarekomponente auf, die die Details von OAuth2 verarbeitet. Ich werde den Entwurf weiter unten in diesem Tutorial ausführlicher beschreiben.

Senden einer nicht autorisierten Anforderung

Führen Sie die App aus, und klicken Sie auf die Schaltfläche API aufrufen . Wenn die Anforderung abgeschlossen ist, sollte im Feld Ergebnis eine Fehlermeldung angezeigt werden. Das liegt daran, dass die Anforderung kein Zugriffstoken enthält, sodass die Anforderung nicht autorisiert ist.

Abbildung der Ergebnisfehlermeldung

Die Schaltfläche API aufrufen sendet eine AJAX-Anforderung an ~/api/values, wodurch eine Web-API-Controlleraktion aufgerufen wird. Hier ist der Abschnitt von JavaScript-Code, der die AJAX-Anforderung sendet. In der Beispiel-App befindet sich der gesamte JavaScript-App-Code in der Scripts\app.js-Datei.

// If we already have a bearer token, set the Authorization header.
var token = sessionStorage.getItem(tokenKey);
var headers = {};
if (token) {
    headers.Authorization = 'Bearer ' + token;
}

$.ajax({
    type: 'GET',
    url: 'api/values/1',
    headers: headers
}).done(function (data) {
    self.result(data);
}).fail(showError);

Bis sich der Benutzer anmeldet, ist kein Bearertoken und daher kein Autorisierungsheader in der Anforderung vorhanden. Dies führt dazu, dass die Anforderung einen Fehler 401 zurückgibt.

Hier ist die HTTP-Anforderung. (Ich habe Fiddler verwendet, um den HTTP-Datenverkehr zu erfassen.)

GET https://localhost:44305/api/values HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Accept-Language: en-US,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/

HTTP-Antwort:

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
WWW-Authenticate: Bearer
Date: Tue, 30 Sep 2014 21:54:43 GMT
Content-Length: 61

{"Message":"Authorization has been denied for this request."}

Beachten Sie, dass die Antwort einen Www-Authenticate-Header enthält, wobei die Herausforderung auf Bearer festgelegt ist. Dies bedeutet, dass der Server ein Bearertoken erwartet.

Registrieren eines Benutzers

Geben Sie im Abschnitt Registrieren der App eine E-Mail und ein Kennwort ein, und klicken Sie auf die Schaltfläche Registrieren .

Sie müssen für dieses Beispiel keine gültige E-Mail-Adresse verwenden, aber eine echte App würde die Adresse bestätigen. (Siehe Erstellen einer sicheren ASP.NET MVC 5-Web-App mit Anmeldung, E-Mail-Bestätigung und Kennwortzurücksetzung.) Verwenden Sie für das Kennwort etwas wie "Password1!", mit Großbuchstaben, Kleinbuchstaben, Zahlen und nicht alphanumerischen Zeichen. Um die App einfach zu halten, habe ich die clientseitige Überprüfung ausgelassen. Wenn also ein Problem mit dem Kennwortformat vorliegt, erhalten Sie einen Fehler 400 (Ungültige Anforderung).

Abbildung des Abschnitts

Die Schaltfläche Registrieren sendet eine POST-Anforderung an ~/api/Account/Register/. Der Anforderungstext ist ein JSON-Objekt, das den Namen und das Kennwort enthält. Hier sehen Sie den JavaScript-Code, der die Anforderung sendet:

var data = {
    Email: self.registerEmail(),
    Password: self.registerPassword(),
    ConfirmPassword: self.registerPassword2()
};

$.ajax({
    type: 'POST',
    url: '/api/Account/Register',
    contentType: 'application/json; charset=utf-8',
    data: JSON.stringify(data)
}).done(function (data) {
    self.result("Done!");
}).fail(showError);

HTTP-Anforderung, wobei $CREDENTIAL_PLACEHOLDER$ ein Platzhalter für das Kennwortschlüssel-Wert-Paar ist:

POST https://localhost:44305/api/Account/Register HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 84

{"Email":"alice@example.com",$CREDENTIAL_PLACEHOLDER1$,$CREDENTIAL_PLACEHOLDER2$"}

HTTP-Antwort:

HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 00:57:58 GMT
Content-Length: 0

Diese Anforderung wird von der AccountController -Klasse verarbeitet. AccountController Intern verwendet ASP.NET Identity, um die Mitgliedschaftsdatenbank zu verwalten.

Wenn Sie die App lokal aus Visual Studio ausführen, werden Benutzerkonten in LocalDB in der Tabelle AspNetUsers gespeichert. Klicken Sie zum Anzeigen der Tabellen in Visual Studio auf das Menü Ansicht, wählen Sie Server Explorer aus, und erweitern Sie dann Datenverbindungen.

Abbildung von Datenverbindungen

Abrufen eines Zugriffstokens

Bisher haben wir noch keine OAuth durchgeführt, aber jetzt sehen wir den OAuth-Autorisierungsserver in Aktion, wenn wir ein Zugriffstoken anfordern. Geben Sie im Bereich Anmelden der Beispiel-App die E-Mail und das Kennwort ein, und klicken Sie auf Anmelden.

Abbildung des Anmeldeabschnitts

Die Schaltfläche Anmelden sendet eine Anforderung an den Tokenendpunkt. Der Text der Anforderung enthält die folgenden Formular-URL-codierten Daten:

  • grant_type: "Password"
  • benutzername: <die E-Mail-Adresse des Benutzers>
  • password: password: <password>

Hier sehen Sie den JavaScript-Code, der die AJAX-Anforderung sendet:

var loginData = {
    grant_type: 'password',
    username: self.loginEmail(),
    password: self.loginPassword()
};

$.ajax({
    type: 'POST',
    url: '/Token',
    data: loginData
}).done(function (data) {
    self.user(data.userName);
    // Cache the access token in session storage.
    sessionStorage.setItem(tokenKey, data.access_token);
}).fail(showError);

Wenn die Anforderung erfolgreich ist, gibt der Autorisierungsserver ein Zugriffstoken im Antworttext zurück. Beachten Sie, dass wir das Token im Sitzungsspeicher speichern, um es später beim Senden von Anforderungen an die API zu verwenden. Im Gegensatz zu einigen Authentifizierungsformen (z. B. cookiebasierte Authentifizierung) schließt der Browser das Zugriffstoken nicht automatisch in nachfolgende Anforderungen ein. Die Anwendung muss dies explizit tun. Das ist gut so, denn es schränkt CSRF-Sicherheitsrisiken ein.

HTTP-Anforderung:

POST https://localhost:44305/Token HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://localhost:44305/
Content-Length: 68

grant_type=password&username=alice%40example.com&password=Password1!

Sie können sehen, dass die Anforderung die Anmeldeinformationen des Benutzers enthält. Sie müssen HTTPS verwenden, um Sicherheit auf Transportebene zu gewährleisten.

HTTP-Antwort:

HTTP/1.1 200 OK
Content-Length: 669
Content-Type: application/json;charset=UTF-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:22:36 GMT

{
  "access_token":"imSXTs2OqSrGWzsFQhIXziFCO3rF...",
  "token_type":"bearer",
  "expires_in":1209599,
  "userName":"alice@example.com",
  ".issued":"Wed, 01 Oct 2014 01:22:33 GMT",
  ".expires":"Wed, 15 Oct 2014 01:22:33 GMT"
}

Aus Gründen der Lesbarkeit habe ich den JSON-Code eingerückt und das Zugriffstoken abgeschnitten, was ziemlich lang ist.

Die access_tokenEigenschaften , token_typeund expires_in werden durch die OAuth2-Spezifikation definiert. Die anderen Eigenschaften (userName, .issuedund .expires) dienen lediglich zu Informationszwecken. Den Code, der diese zusätzlichen Eigenschaften hinzufügt, finden Sie in der TokenEndpoint -Methode in der Datei /Providers/ApplicationOAuthProvider.cs.

Senden einer authentifizierten Anforderung

Nachdem wir nun über ein Bearertoken verfügen, können wir eine authentifizierte Anforderung an die API senden. Dies erfolgt durch Festlegen des Autorisierungsheaders in der Anforderung. Klicken Sie erneut auf die Schaltfläche API aufrufen , um dies anzuzeigen.

Bild nach dem Klicken auf die Schaltfläche

HTTP-Anforderung:

GET https://localhost:44305/api/values/1 HTTP/1.1
Host: localhost:44305
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Accept: */*
Authorization: Bearer imSXTs2OqSrGWzsFQhIXziFCO3rF...
X-Requested-With: XMLHttpRequest

HTTP-Antwort:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Wed, 01 Oct 2014 01:41:29 GMT
Content-Length: 27

"Hello, alice@example.com."

Abmelden

Da der Browser die Anmeldeinformationen oder das Zugriffstoken nicht zwischenspeichert, ist das Abmelden einfach eine Frage des "Vergessens" des Tokens, indem es aus dem Sitzungsspeicher entfernt wird:

self.logout = function () {
    sessionStorage.removeItem(tokenKey)
}

Grundlegendes zur Projektvorlage "Einzelne Konten"

Wenn Sie in der Projektvorlage ASP.NET Webanwendung die Option Einzelkonten auswählen, umfasst das Projekt Folgendes:

  • Ein OAuth2-Autorisierungsserver.
  • Ein Web-API-Endpunkt zum Verwalten von Benutzerkonten
  • Ein EF-Modell zum Speichern von Benutzerkonten.

Im Folgenden finden Sie die Standard Anwendungsklassen, die diese Features implementieren:

  • AccountController. Stellt einen Web-API-Endpunkt zum Verwalten von Benutzerkonten bereit. Die Register Aktion ist die einzige, die wir in diesem Tutorial verwendet haben. Andere Methoden der -Klasse unterstützen die Kennwortzurücksetzung, anmeldungen in sozialen Netzwerken und andere Funktionen.
  • ApplicationUser, definiert in /Models/IdentityModels.cs. Diese Klasse ist das EF-Modell für Benutzerkonten in der Mitgliedschaftsdatenbank.
  • ApplicationUserManager, definiert in /App_Start/IdentityConfig.cs Diese Klasse wird von UserManager abgeleitet und führt Vorgänge für Benutzerkonten aus, z. B. das Erstellen eines neuen Benutzers, das Überprüfen von Kennwörtern usw. und speichert Änderungen an der Datenbank automatisch.
  • ApplicationOAuthProvider. Dieses Objekt wird in die OWIN-Middleware eingebunden und verarbeitet Ereignisse, die von der Middleware ausgelöst werden. Er wird von OAuthAuthorizationServerProvider abgeleitet.

Abbildung von Standard Anwendungsklassen

Konfigurieren des Autorisierungsservers

In StartupAuth.cs konfiguriert der folgende Code den OAuth2-Autorisierungsserver.

PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
    TokenEndpointPath = new PathString("/Token"),
    Provider = new ApplicationOAuthProvider(PublicClientId),
    AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
    AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
    // Note: Remove the following line before you deploy to production:
    AllowInsecureHttp = true
};

// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);

Die TokenEndpointPath -Eigenschaft ist der URL-Pfad zum Autorisierungsserverendpunkt. Dies ist die URL, die von der App zum Abrufen der Bearertoken verwendet wird.

Die Provider -Eigenschaft gibt einen Anbieter an, der an die OWIN-Middleware angibt und von der Middleware ausgelöste Ereignisse verarbeitet.

Dies ist der grundlegende Ablauf, wenn die App ein Token abrufen möchte:

  1. Zum Abrufen eines Zugriffstokens sendet die App eine Anforderung an ~/Token.
  2. Die OAuth-Middleware ruft GrantResourceOwnerCredentials den Anbieter auf.
  3. Der Anbieter ruft die ApplicationUserManager auf, um die Anmeldeinformationen zu überprüfen und eine Anspruchsidentität zu erstellen.
  4. Wenn dies erfolgreich ist, erstellt der Anbieter ein Authentifizierungsticket, das zum Generieren des Tokens verwendet wird.

Diagramm des Autorisierungsflusses

Die OAuth-Middleware weiß nichts über die Benutzerkonten. Der Anbieter kommuniziert zwischen der Middleware und ASP.NET Identity. Weitere Informationen zum Implementieren des Autorisierungsservers finden Sie unter OWIN OAuth 2.0-Autorisierungsserver.

Konfigurieren der Web-API für die Verwendung von Bearertoken

In der WebApiConfig.Register -Methode richtet der folgende Code die Authentifizierung für die Web-API-Pipeline ein:

config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

Die HostAuthenticationFilter-Klasse ermöglicht die Authentifizierung mithilfe von Bearertoken.

Die SuppressDefaultHostAuthentication-Methode weist die Web-API an, alle Authentifizierungen zu ignorieren, die erfolgt, bevor die Anforderung die Web-API-Pipeline erreicht, entweder durch IIS oder durch OWIN-Middleware. Auf diese Weise können Sie die Authentifizierung der Web-API ausschließlich auf Bearertoken einschränken.

Hinweis

Insbesondere kann der MVC-Teil Ihrer App die Formularauthentifizierung verwenden, die Anmeldeinformationen in einem Cookie speichert. Die cookiebasierte Authentifizierung erfordert die Verwendung von Antifälschungstoken, um CSRF-Angriffe zu verhindern. Dies ist ein Problem für Web-APIs, da es für die Web-API keine bequeme Möglichkeit gibt, das Antifälschungstoken an den Client zu senden. (Weitere Informationen zu diesem Problem finden Sie unter Verhindern von CSRF-Angriffen in der Web-API.) Durch aufrufen von SuppressDefaultHostAuthentication wird sichergestellt, dass die Web-API nicht anfällig für CSRF-Angriffe aus in Cookies gespeicherten Anmeldeinformationen ist.

Wenn der Client eine geschützte Ressource anfordert, geschieht folgendes in der Web-API-Pipeline:

  1. Der Filter HostAuthentication ruft die OAuth-Middleware auf, um das Token zu überprüfen.
  2. Die Middleware konvertiert das Token in eine Anspruchsidentität.
  3. An diesem Punkt wird die Anforderung authentifiziert , aber nicht autorisiert.
  4. Der Autorisierungsfilter untersucht die Anspruchsidentität. Wenn die Ansprüche den Benutzer für diese Ressource autorisieren, wird die Anforderung autorisiert. Standardmäßig autorisiert das Attribut [Authorize] jede anforderung, die authentifiziert ist. Sie können jedoch nach Rolle oder anderen Ansprüchen autorisieren. Weitere Informationen finden Sie unter Authentifizierung und Autorisierung in der Web-API.
  5. Wenn die vorherigen Schritte erfolgreich sind, gibt der Controller die geschützte Ressource zurück. Andernfalls erhält der Client den Fehler 401 (Nicht autorisiert).

Diagramm: Anforderung einer geschützten Ressource durch den Client

Zusätzliche Ressourcen