ASP.NET

Aktivieren und Anpassen der Sicherheit von ASP.NET-Web-API-Diensten

Peter Vogel

Betrachtet man das gängigste Szenario – nämlich einen JavaScript-Zugriff auf einen Web-API-Dienst auf derselben Website –, erübrigt sich eine Sicherheitsdiskussion für die ASP.NET-Web-API nahezu. Denn hier haben Sie wahrscheinlich bereits alle für Ihre Dienste erforderlichen Sicherheitsaspekte bereitgestellt. (Immer vorausgesetzt, dass Sie Ihre Benutzer authentifizieren und den Zugriff auf Web Forms/Views, die den JavaScript-Code für Ihre Dienste enthalten, autorisieren.) Das liegt daran, dass ASP.NET die für die Überprüfung von Seitenaufrufen verwendeten Cookies und Authentifizierungsinformationen im Rahmen von clientseitigen JavaScript-Anforderungen für Ihre Dienstmethoden verwendet. Wobei es hier eine nicht zu vernachlässigende Ausnahme gibt: ASP.NET schützt nicht automatisch vor websiteübergreifenden Anforderungsfälschungen (CSRF). Näheres hierzu aber später.

Neben CSRF-Angriffen gibt es zwei weitere Szenarios, in denen Sie sich der Sicherheit Ihrer Web-API-Dienste zuwenden sollten. Erstens, wenn Ihr Dienst von einem Client verwendet wird, der sich nicht auf derselben Website befindet wie Ihre API-Controller. Solche Clients werden nicht über die Formularauthentifizierung authentifiziert und übernehmen nicht die Cookies und Token, mit denen ASP.NET den Zugriff auf Ihre Dienste steuert.

Zweitens, wenn Sie Ihre Dienste mit einer zusätzlichen Autorisierung versehen möchten, die über die von ASP.NET bereitgestellte Sicherheit hinausgeht. Die Standardsicherheit von ASP.NET basiert auf der Identität, die ASP.NET der Anforderung während der Authentifizierung zuweist. Eventuell möchten Sie diese Identität aber erweitern und den Zugriff auf die Dienste anhand von anderen Merkmalen als dem Namen oder der Rolle der Identität autorisieren.

Die Web-API stellt eine Reihe von Optionen bereit, mit denen Sie den Anforderungen beider Szenarios entsprechen können. Zwar betrachte ich die Sicherheit im Kontext der Annahme von Web-API-Anforderungen (da die Web-API auf derselben ASP.NET-Umgebung basiert wie Web Forms und MVC), die Tools, die ich in diesem Artikel vorstelle, werden aber allen vertraut sein, die sich detailliert mit der Sicherheit in Web Forms oder MVC beschäftigen.

Es gibt jedoch einen Nachteil: Die Web-API stellt zwar verschiedene Authentifizierungs- und Autorisierungsoptionen bereit, die Sicherheit beginnt aber auf Hostebene, also entweder bei IIS oder einem selbst erstellten Host, falls Sie Self-Hosting betreiben. Wenn Sie also beispielsweise die Verbindung zwischen einem Web-API-Dienst und dem Client schützen möchten, sollten Sie zumindest SSL aktivieren. Dies fällt allerdings in den Bereich des Websiteadministrators und ist nicht die Aufgabe des Entwicklers. Im vorliegenden Artikel werde ich nicht weiter auf den Host eingehen; vielmehr konzentriere ich mich darauf, was ein Entwickler für die Absicherung eines Web-API-Diensts tun kann und muss (sowie darauf, ob die hier vorgestellten Tools bei ein- und ausgeschaltetem SSL funktionieren).

Verhindern von websiteübergreifenden Anforderungsfälschungen

Wenn ein Benutzer mittels Formularauthentifizierung auf eine ASP.NET-Website zugreift, generiert ASP.NET ein Cookie, das eine Authentifizierung des Benutzers vorschreibt. Der Browser sendet das Cookie dann bei jeder nachfolgenden Anforderung der Website erneut, unabhängig davon, woher die Anforderung stammt. Wie bei allen anderen Authentifizierungsschemas, bei denen der Browser automatisch zuvor empfangene Authentifizierungsinformationen sendet, wird dadurch Ihre Website anfällig für CSRF-Angriffe. Wenn der Benutzer nach der Bereitstellung des Sicherheitscookies für den Browser durch Ihre Website eine schädliche Website besucht, kann diese Website Dienstanforderungen senden und sich dabei an das zuvor empfangene Authentifizierungscookie anhängen.

Zur Verhinderung von CSRF-Angriffen müssen Sie Fälschungssicherheitstoken auf dem Server generieren und diese für clientseitige Aufrufe in die Seite einbetten. Hierzu stellt Microsoft die AntiForgery-Klasse mit einer GetToken-Methode bereit. Diese generiert benutzerspezifische Token für den Sender der Anforderung (der natürlich auch anonym sein kann). Die beiden Token werden durch den nachfolgenden Code generiert und in ASP.NET MVC in „ViewBag“ abgelegt, sodass sie in der Ansicht verwendet werden können:

[Authorize(Roles="manager")]
public ActionResult Index()
{
  string cookieToken;
  string formToken;
  AntiForgery.GetTokens(null, out cookieToken, out formToken);
  ViewBag.cookieToken = cookieToken;
  ViewBag.formToken = formToken;
  return View("Index");
}

Bei allen JavaScript-Aufrufen an den Server müssen die Token als Teil der Anforderung zurückgegeben werden (denn auf einer CSRF-Website sind diese Token nicht vorhanden und können folglich nicht zurückgegeben werden). Durch den nachfolgenden Code wird in einer Ansicht dynamisch ein JavaScript-Aufruf generiert, der die Token zu den Headern der Anforderung hinzufügt:

$.ajax("http://phvis.com/api/Customers",{
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': '@ViewBag.formToken',
    'cookieToken': '@ViewBag.cookieToken' }});

Etwas komplexer wäre die Verwendung eines dezenten JavaScript-Codes, wobei Sie die Token in verborgene Felder der Ansicht einbetten. Als ersten Schritt in diesem Prozess würden Sie die Token zum ViewData-Wörterbuch hinzufügen:

ViewData["cookieToken"] = cookieToken;
ViewData["formToken"] = formToken;

Jetzt können Sie in der Ansicht die Daten in verborgene Felder einbetten. Für die Erstellung des richtigen Eingabetags muss der Hidden-Methode des HtmlHelper-Objekts nur der Wert eines ViewDate-Schlüssels übergeben werden:

@Html.Hidden("formToken")

Das resultierende Eingabetag verwendet den ViewData-Schlüssel als name- und id-Attribut des Tags und legt die aus dem ViewData-Wörterbuch abgerufenen Daten im value-Attribut des Tags ab. Das im vorherigen Codebeispiel generierte Eingabetag sieht wie folgt aus:

    <input id="formToken" name="formToken" type="hidden" value="...token..." />

Der (in einer von der Ansicht getrennten Datei gespeicherte) JavaScript-Code kann die Werte aus den Eingabetags abrufen und im Ajax-Aufruf verwenden:

$.ajax("http://localhost:49226/api/Customers", {
  type: "get",
  contentType: "application/json",
  headers: {
    'formToken': $("#formToken").val(),
    'cookieToken': $("#cookieToken").val()}});

Dasselbe Ergebnis erzielen Sie auf einer ASP.NET Web Forms-Website, indem Sie die RegisterClientScriptBlock-Methode auf das ClientScriptManager-Objekt anwenden (dieses lässt sich über die ClientScript-Eigenschaft der Seite abrufen), um JavaScript-Code mit den eingebetteten Token einzufügen:

string CodeString = "function CallService(){" +
  "$.ajax('http://phvis.com/api/Customers',{" +
  "type: 'get', contentType: 'application/json'," +
  "headers: {'formToken': '" & formToken & "',” +
  "'cookieToken': '" & cookieToken & "'}});}"
this.ClientScript.RegisterClientScriptBlock(
  typeOf(this), "loadCustid", CodeString, true);

Abschließend müssen Sie noch die Token auf dem Server überprüfen, wenn sie durch den JavaScript-Aufruf zurückgegeben werden. Benutzer von Visual Studio 2012, die das Update 2012.2 für ASP.NET und Web Tools installiert haben, werden feststellen, dass die neue Vorlage für Einzelseitenanwendungen (Single-Page Application, SPA) einen ValidateHttpAntiForgeryToken-Filter enthält, der für Web-API-Methoden verwendet werden kann. Ohne diesen Filter müssen Sie die Token abrufen und sie an die Validate-Methode der AntiForgery-Klasse übergeben (wobei die Validate-Methode eine Ausnahme auslöst, wenn die Token ungültig sind oder von einem anderen Benutzer generiert wurden). Wenn das in Abbildung 1 dargestellte Codebeispiel in einer Web-API-Dienstmethode verwendet wird, werden die Token aus den Headern abgerufen und überprüft.

Abbildung 1 – Überprüfen von CSRF-Token in einer Dienstmethode

public HttpResponseMessage Get(){
  if (Request.Headers.TryGetValues("cookieToken", out tokens))
  {
    string cookieToken = tokens.First();
    Request.Headers.TryGetValues("formToken", out tokens);
    string formToken = tokens.First();
    AntiForgery.Validate(cookieToken, formToken);
  }
  else
  {
    HttpResponseMessage hrm =
      new HttpResponseMessage(HttpStatusCode.Unauthorized);
    hrm.ReasonPhrase = "CSRF tokens not found";
    return hrm;
  } 
  // ... Code to process request ...

Wenn Sie anstelle von methodeninternem Code „ValidateHttpAntiForgeryToken“ verwenden, erfolgt die Verarbeitung zu einem früheren Zeitpunkt im Zyklus (z. B. vor der Modellbindung), was eine gute Sache ist.

Warum kein OAuth?

In diesem Artikel gehe ich OAuth geflissentlich aus dem Weg. In der OAuth-Spezifikation wird festgelegt, wie Token durch einen Client von einem Drittserver abgerufen und an einen Dienst gesendet werden können, der wiederum das Token anhand des Drittservers überprüft. Zu erörtern, wie entweder über den Client oder den Dienst ein Zugriff auf den Anbieter eines OAuth-Tokens erfolgen kann, würde den Rahmen des vorliegenden Artikels sprengen.

Zudem eignet sich die erste OAuth-Version nicht besonders gut für die Web-API. Vermutlich wird die Web-API hauptsächlich für schlankere Anforderungen auf der Basis von REST und JSON verwendet. Das macht die erste OAuth-Version für Web-API-Dienste unattraktiv. Die von der ersten OAuth-Version angegebenen Token basieren auf XML und sind sehr unhandlich. Aber zum Glück wurde mit OAuth 2.0 eine Spezifikation für ein schlankeres JSON-Token eingeführt, das im Vergleich zu den Token der früheren Versionen kompakter ist. Wahrscheinlich lassen sich mit den im vorliegenden Artikel erläuterten Verfahren beliebige OAuth-Token verarbeiten, die an Ihren Dienst gesendet werden.

Standardauthentifizierung

Eine der Hauptaufgaben beim Absichern eines Web-API-Diensts besteht (neben der Autorisierung) in der Authentifizierung. Es wird angenommen, dass andere Aspekte wie Datenschutz auf der Hostebene behandelt werden.

Idealerweise erfolgen Authentifizierung und Autorisierung so früh wie möglich in der Web-API-Pipeline; so vermeiden Sie Verarbeitungszyklen für eine Anforderung, die Sie abzulehnen gedenken. Die in diesem Artikel vorgestellten Authentifizierungslösungen werden zu einem sehr frühen Zeitpunkt in der Pipeline eingesetzt, quasi direkt nach Eingang der Anforderung. Mit diesen Verfahren können Sie die Authentifizierung in beliebige Benutzerlisten integrieren, die Sie bereits verwalten. Die vorgestellten Autorisierungsverfahren können in unterschiedlichen Phasen der Pipeline angewendet werden (z. B. erst am Ende in der Dienstmethode selbst), und sie funktionieren mit einer Authentifizierung, bei der Anforderungen anhand von anderen Kriterien als dem Benutzernamen oder der Rolle autorisiert werden.

Es besteht die Möglichkeit der Unterstützung von Clients, die die Formularauthentifizierung nicht durchlaufen haben. Stellen Sie hierbei Ihre eigene Authentifizierungsmethode in einem benutzerdefinierten HTTP-Modul bereit (ich gehe hier weiterhin davon aus, dass Sie keine Authentifizierung anhand von Windows-Konten durchführen, sondern anhand einer eigenen Liste gültiger Benutzer). Die Verwendung eines HTTP-Moduls bietet zwei wesentliche Vorteile: Module sind an der HTTP-Protokollierung und -Überwachung beteiligt und werden in einer frühen Phase der Pipeline aufgerufen. Soweit die positiven Seiten. Module bieten aber auch zwei Nachteile: Zum einen werden sie global auf alle Anforderungen für die Website angewendet (und nicht nur auf Web-API-Anforderungen), zum anderen muss bei der Verwendung von Authentifizierungsmodulen der Dienst in IIS gehostet werden. Im weiteren Verlauf dieses Artikels werde ich die Verwendung von delegierenden Handlern erläutern, die nur für Web-API-Anforderungen aufgerufen werden und die sich Hosts gegenüber agnostisch verhalten.

Bei diesem Beispiel gehe ich bei der Verwendung eines HTTP-Moduls davon aus, dass IIS die Standardauthentifizierung einsetzt und die Anmeldeinformationen zur Benutzerauthen­tifizierung (Benutzername und Kennwort) vom Client gesendet werden (ich werde in diesem Artikel lediglich auf die Verwendung von Clientzertifikaten, nicht aber auf die Windows-Zertifizierung eingehen). Ferner gehe ich davon aus, dass der Web-API-Dienst mittels eines Authorize-Attributs ähnlich dem folgenden abgesichert wird, das einen Benutzer angibt:

public class CustomersController : ApiController
{
  [Authorize(Users="Peter")]
  public Customer Get()
  {

Bei der Erstellung eines benutzerdefinierten HTTP-Moduls für die Autorisierung müssen Sie Ihrem Dienstprojekt als erstes eine Klasse zur Implementierung der Schnittstellen „IHttpModule“ und „IDisposable“ hinzufügen. Verbinden Sie in der Init-Methode der Klasse zwei Ereignisse des HttpApplication-Objekts, das an die Methode übergeben wird. Die an das AuthenticateRequest-Ereignis angefügte Methode wird bei der Darstellung der Anmeldeinformationen des Clients aufgerufen. Vergessen Sie nicht, ebenfalls die EndRequest-Methode zu verbinden. Erst dann kann die Meldung generiert werden, anhand derer der Client seine Anmeldeinformationen versendet. Ferner ist eine Dispose-Methode erforderlich; diese benötigt allerdings keinen Inhalt, um den im Folgenden verwendeten Code ausführen zu können:

public class PHVHttpAuthentication : IHttpModule, IDisposable
{
  public void Init(HttpApplication context)
  {
    context.AuthenticateRequest += AuthenticateRequests;
    context.EndRequest += TriggerCredentials;
  }
  public void Dispose()
  {
  }

Als Reaktion auf einen in die HTTP-Antwort eingefügten WWW-­Authenticate-Header sendet ein HTTP-Client Anmeldeinformationen. Sie sollten diesen Header einfügen, wenn durch eine Anforderung der Statuscode 401 generiert wird (ASP.NET generiert den Antwortcode 401, wenn dem Client der Zugriff auf einen gesicherten Dienst verweigert wird). Der Header muss einen Hinweis auf die verwendete Authentifizierungsmethode und den Bereich enthalten, für den die Authentifizierung gilt (dieser kann eine beliebige Zeichenfolge sein, die dazu dient, unterschiedliche Serverbereiche für den Browser zu kennzeichnen). In die mit dem EndRequest-Ereignis verbundene Methode packen Sie den Code zum Senden dieser Meldung. Im nachfolgenden Beispiel wird eine Meldung generiert, die angibt, dass im Bereich PHVIS die Standardauthentifizierung verwendet wird:

private static void TriggerCredentials(object sender, EventArgs e)
{
  HttpResponse resp = HttpContext.Current.Response;
  if (resp.StatusCode == 401)
  {
    resp.Headers.Add("WWW-Authenticate", @"Basic realm='PHVIS'");
  }
}

In der Methode, die Sie mit der AuthenticateRequest-Methode verbunden haben, müssen Sie die Autorisierungsheader abrufen, die der Client infolge des Empfangs der WWW-Authenticate-Meldung 401 sendet:

private static void AuthenticateRequests(object sender,
  EventArgs e)
{
  string authHeader =     
    HttpContext.Current. Request.Headers["Authorization"];
  if (authHeader != null)
  {

Nachdem Sie (weiterhin unter der Annahme, dass die Website die Standardauthentifizierung verwendet) ermittelt haben, dass die Autorisierungsheaderelemente vom Client übergeben wurden, müssen Sie die Daten analysieren, die den Benutzernamen und das Kennwort enthalten. Benutzername und Kennwort sind Base64-codiert und werden durch einen Doppelpunkt voneinander getrennt. Dieser Code ruft den Benutzernamen und das Kennwort ab und legt beide in einem Zeichenfolgenarray mit zwei Positionen ab:

AuthenticationHeaderValue authHeaderVal =
  AuthenticationHeaderValue.Parse(authHeader);
if (authHeaderVal.Parameter != null)
{
  byte[] unencoded = Convert.FromBase64String(
    authHeaderVal.Parameter);
  string userpw =
    Encoding.GetEncoding("iso-8859-1").GetString(unencoded);
  string[] creds = userpw.Split(':');

Durch diesen Code wird veranschaulicht, dass Benutzernamen und Kennwörter unverschlüsselt gesendet werden. Wenn Sie also SSL nicht aktivieren, können Ihre Benutzernamen und Kennwörter leicht abgegriffen werden (wobei dieser Code auch mit aktiviertem SSL funktioniert).

Als nächsten Schritt überprüfen Sie Benutzernamen und Kennwort nach einer beliebigen für Sie sinnvollen Methode. Unabhängig von der gewählten Methode (der von mir nachfolgend bereitgestellte Code ist möglicherweise zu einfach) erstellen Sie abschließend eine Identität für den Benutzer, die im Autorisierungsprozess in einer späteren Phase der ASP.NET-Pipeline verwendet wird.

Damit diese Identitätsinformationen durch die Pipeline übergeben werden können, erstellen Sie ein GenericIdentity-Objekt mit dem Namen der Identität, die Sie dem Benutzer zuweisen möchten (im nachfolgenden Codebeispiel habe ich angenommen, dass die Identität dem im Header gesendeten Benutzernamen entspricht). Legen Sie das GenericIdentity-Objekt nach der Erstellung in der CurrentPrincipal-Eigenschaft der Thread-Klasse ab. In ASP.NET wird zudem ein zweiter Sicherheitskontext im HttpContext-Objekt verwaltet. Wenn Sie einen IIS-Host verwenden, müssen Sie zu Unterstützung dieses Kontexts die User-Eigenschaft in der Current-Eigenschaft des HttpContext-Objekts auch für das GenericIdentity-Objekt festlegen:

if (creds[0] == "Peter" && creds[1] == "pw")
{
  GenericIdentity gi = new GenericIdentity(creds[0]);
  Thread.CurrentPrincipal = new GenericPrincipal(gi, null);
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Zu Unterstützung der rollenbasierten Sicherheit müssen Sie als zweiten Parameter ein Array von Rollennamen an den GenericPrincipal-Konstruktor übergeben. Im nachfolgenden Beispiel wird jedem Benutzer die Rolle „Manager“ und „Administrator“ zugewiesen:

string[] roles = "manager,admin".Split(',');
Thread.CurrentPrincipal = new GenericPrincipal(gi, roles);

Wenn Sie Ihr HTTP-Modul in die Websiteverarbeitung integrieren möchten, d. h. in die Datei „web.config“ Ihres Projekts, verwenden Sie im modules-Element das add-Tag. Das type-Attribut des add-Tags muss mit einer Zeichenfolge belegt werden, die aus dem vollqualifizierten Klassennamen gefolgt vom Assemblynamen Ihres Moduls besteht:

<modules>
  <add name="myCustomerAuth"
    type="SecureWebAPI.PHVHttpAuthentication, SecureWebAPI"/>
</modules>

Das erstellte GenericIdentity-Objekt funktioniert mit dem Authorize-Attribut von ASP.NET. Sie können auch über eine Dienstmethode auf das GenericIdentity-Objekt zugreifen, um Autorisierungsmaßnahmen durchzuführen. Zum Beispiel können Sie unterschiedliche Dienste für angemeldete und anonyme Benutzer bereitstellen, indem Sie durch Überprüfung der IsAuthenticated-Eigenschaft des GenericIdentity-Objekts herausfinden, ob ein Benutzer authentifiziert ist (bei anonymen Benutzern wird „IsAuthenticated“ zu „false“ ausgewertet):

if (Thread.CurrentPrincipal.Identity.IsAuthenticated)
{

Einfacher können Sie das GenericIdentity-Objekt über die User-Eigenschaft abrufen:

if (User.Identity.IsAuthenticated)
{

Erstellen eines kompatiblen Clients

Damit die Dienste, die durch dieses Modul geschützt werden, auch von Clients ohne JavaScript genutzt werden können, muss der Client einen zulässigen Benutzernamen und ein zulässiges Kennwort bereitstellen. Diese Anmeldeinformationen können Sie mit dem .NET-HTTP-Client bereitstellen, indem Sie zunächst ein HttpClientHandler-Objekt erstellen und dessen Credentials-Eigenschaft auf ein NetworkCredential-Objekt festzulegen, das den Benutzernamen und das Kennwort enthält (bzw. die UseDefaultCredentials-Eigenschaft des HttpClientHandler-Objekts auf „true“ setzen, um die Windows-Anmeldeinformationen des aktuellen Benutzers zu verwenden). Anschließend erstellen Sie Ihr HttpClient-Objekt durch Übergabe des HttpClientHandler-Objekts:

HttpClientHandler hch = new HttpClientHandler();
hch.Credentials = new NetworkCredential ("Peter", "pw");
HttpClient hc = new HttpClient(hch);

Wenn diese Konfiguration abgeschlossen ist, können Sie Ihre Anforderung für den Dienst ausgeben. Der HTTP-Client stellt die Anmeldeinformationen des Benutzers erst dar, wenn ihm der Zugriff auf den Dienst verweigert wird und er die WWW-Authenticate-Meldung empfangen hat. Wenn die vom HttpClient-Objekt bereitgestellten Anmeldeinformationen unzulässig sind, gibt der Dienst eine HttpResponseMessage zurück, deren Ergebnissatzes den Statuscode „unauthenticated“ aufweist.

Mit dem folgenden Code wird ein Dienst über die GetAsync-Methode aufgerufen, eine Überprüfung auf ein erfolgreiches Ergebnis durchgeführt und (im negativen Fall) der vom Dienst zurückgegebene Statuscode angezeigt:

hc.GetAsync("http://phvis.com/api/Customers").ContinueWith(r =>
{
  HttpResponseMessage hrm = r.Result;
  if (hrm.IsSuccessStatusCode)
  {
    // ... Process response ...
  }
  else
  {
    MessageBox.Show(hrm.StatusCode.ToString());
  }
});

Sofern Sie, wie ich es hier getan habe, auf Clients ohne ­JavaScript den ASP.NET-Anmeldeprozess umgehen, werden keine Authentifizierungscookies erstellt, und jede Anforderung des Clients wird einzeln überprüft. Zur Reduzierung des Aufwands, der mit der wiederholten Überprüfung der durch den Client bereitgestellten Anmeldeinformationen verbunden ist, sollten Sie eine Zwischenspeicherung der beim Dienst abgerufenen Anmeldeinformationen (sowie die Verwendung Ihrer Dispose-Methode zum Verwerfen der zwischengespeicherten Anmeldeinformationen) in Betracht ziehen.

Arbeiten mit Clientzertifikaten

Mit folgendem Code rufen Sie in einem HTTP-Modul ein Clientzertifikatobjekt ab (bzw. stellen Sie das Vorhandensein und die Gültigkeit des Zertifikats sicher):

System.Web.HttpClientCertificate cert =
  HttpContext.Current.Request.ClientCertificate;
if (cert!= null && cert.IsPresent && cert.IsValid)
{

Im weiteren Verlauf der Verarbeitungspipeline – etwa in einer Dienstmethode – rufen Sie mit folgendem Code das Zertifikatobjekt ab (bzw. überprüfen Sie das Vorhandensein des Objekts):

X509Certificate2 cert = Request.GetClientCertificate();
if (cert!= null)
{

Wenn ein gültiges Zertifikat vorhanden ist, können Sie seine Eigenschaften zusätzlich nach bestimmten Werten durchsuchen (z. B. Antragsteller oder Aussteller).

Wenn Sie Zertifikate mit einem HTTP-Client senden möchten, müssen Sie anstelle eines HttpClientHandler-Objekts zuerst ein WebRequestHandler-Objekt erstellen („WebRequestHandler“ bietet im Vergleich zu „HttpClientHandler“ mehr Konfigurationsoptionen):

WebRequestHandler wrh = new WebRequestHandler();

Wenn Sie im WebRequestHandler-Objekt in der ClientCertificateOption-Aufzählung unter „ClientCertificateOptions“ den Wert „Automatic“ einstellen, durchsucht der HTTP-Client automatisch die Zertifikatspeicher des Clients:

wrh.ClientCertificateOptions = ClientCertificateOption.Manual;

Standardmäßig muss der Client allerdings ausdrücklich über den Code Zertifikate an das WebRequestHandler-Objekt anhängen. Sie können das Zertifikat aus einem der Zertifikatspeicher des Clients abrufen, wie im folgenden Beispiel dargestellt; hier wird mithilfe des Namens des Ausstellers ein Zertifikat aus dem Speicher von „CurrentUser“ abgerufen:

X509Store certStore;
X509Certificate x509cert;
certStore = new X509Store(StoreName.My, 
  StoreLocation.CurrentUser);  
certStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
x509cert = certStore.Certificates.Find(
  X509FindType.FindByIssuerName, "PHVIS", true)[0];
store.Close();

Wenn dem Benutzer ein Clientzertifikat gesendet wurde, das aus bestimmten Gründen nicht zu seinem Zertifikatspeicher hinzugefügt wird, können Sie über die Zertifikatdatei mit folgendem Code ein X509Certificate-Objekt erstellen:

x509cert = new X509Certificate2(@"C:\PHVIS.pfx");

Unabhängig von der Art der Erstellung des X509Certificate-Objekts bestehen die abschließenden Schritte des Clients darin, das Zertifikat zur ClientCertificates-Sammlung des WebRequestHandler hinzuzufügen und dann anhand des konfigurierten Web­RequestHandler den HTTP-Client zu erstellen:

wrh.ClientCertificates.Add(x509cert);
hc = new HttpClient(wrh);

Autorisieren in einer Self-Hosting-Umgebung

Sie können in einer Self-Hosting-Umgebung zwar kein HTTP-Modul verwenden, der Prozess der Absicherung von Anforderungen in einem frühen Stadium der Verarbeitungspipeline entspricht aber dem eines Self-Hosting-Diensts: Abrufen der Anmeldeinformationen aus der Anforderung, Authentifizierung der Anforderung anhand dieser Informationen und Erstellen einer Identität und Übergeben der Identität an die CurrentPrincipal-Eigenschaft des aktuellen Threads. Am einfachsten ist es, ein Validierungssteuerelement für Benutzernamen und Kennwort zu erstellen. Wenn Sie weitere Überprüfungsmaßnahmen für die Kombination aus Benutzernamen und Kennwort durchführen möchten, können Sie einen delegierenden Handler erstellen. Als erstes schaue ich mir die Integration eines Validierungssteuerelement für Benutzernamen und Kennwort an.

Für die Erstellung eines Validierungssteuerelements müssen Sie (weiterhin unter der Annahme, dass die Standardauthentifizierung verwendet wird) eine Klasse erstellen, die von „User­NamePasswordValidator“ erbt (hierzu müssen Sie der System.IdentityModel-Bibliothek einen Verweis zu Ihrem Projekt hinzufügen). Als einzige Methode der Basisklasse müssen Sie die Validate-Methode überschreiben; dieser werden der Benutzername und das Kennwort übergeben, das der Client an den Dienst sendet. Ähnlich wie zuvor müssen Sie nach der Überprüfung von Benutzernamen und Kennwort ein GenericPrincipal-Objekt erstellen und anhand dieses Objekts die CurrentPrincipal-Eigenschaft der Thread-Klasse festlegen (und nicht die HttpContext User-Eigenschaft, da Sie keinen Host-IIS verwenden):

public class PHVValidator :
  System.IdentityModel.Selectors.UserNamePasswordValidator
{
  public override void Validate(string userName, string password)
  {
    if (userName == "Peter" && password == "pw")
    {
      GenericIdentity gi = new GenericIdentity(username, null);
      Thread.CurrentPrincipal = gi;
    }

Durch den folgenden Code wird ein Host für einen Controller namens „Customers“ mit dem Endpunkt „http://phvis.com/MyServices“ erstellt und ein neues Validierungssteuerelement angegeben:

partial class PHVService : ServiceBase
{
  private HttpSelfHostServer shs;
  protected override void OnStart(string[] args)
  {
    HttpSelfHostConfiguration hcfg =
      new HttpSelfHostConfiguration("http://phvis.com/MyServices");
    hcfg.Routes.MapHttpRoute("CustomerServiceRoute",
      "Customers", new { controller = "Customers" });
    hcfg.UserNamePasswordValidator = new PHVValidator;       
    shs = new HttpSelfHostServer(hcfg);
    shs.OpenAsync();

Nachrichtenhandler

Wenn Sie weitere Überprüfungsmaßnahmen für den Benutzernamen und das Kennwort durchführen möchten, können Sie einen benutzerdefinierten Web-API-Nachrichtenhandler erstellen. Nachrichtenhandler bieten im Vergleich zu einem HTTP-Modul folgende Vorteile: Sie sind nicht an IIS gebunden, daher können die in einem Nachrichtenhandler angewendeten Sicherheitseinstellungen mit allen Hosts verwendet werden; Nachrichtenhandler werden nur von der Web-API verwendet und stellen eine einfache Möglichkeit der Autorisierung (und Zuweisung von Identitäten) für Dienste dar, für die Sie einen anderen Prozess als für Ihre Websiteseiten verwenden; Nachrichtenhandler lassen sich zu bestimmten Routen zuweisen, sodass Ihr Sicherheitscode nur bei Bedarf aufgerufen wird.

Sie erstellen einen Nachrichtenhandler, indem Sie zunächst eine Klasse schreiben, die von „DelegatingHandler“ erbt, und anschließend deren SendAsync-Methode überschreiben:

public class PHVAuthorizingMessageHandler: DelegatingHandler
{
  protected override System.Threading.Tasks.Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
      System.Threading.CancellationToken cancellationToken)
  {

Innerhalb dieser Methode können Sie (unter der Annahme, dass Sie einen Handler pro Route erstellen) die InnerHandler-Eigenschaft von „DelegatingHandler“ so einstellen, dass der Handler innerhalb der Pipeline mit anderen Handlern verknüpft werden kann:

HttpConfiguration hcon = request.GetConfiguration();
InnerHandler = new HttpControllerDispatcher(hcon);

In diesem Beispiel gehe ich davon aus, dass die Abfragezeichenfolge einer gültigen Anforderung ein einfaches Token enthalten muss (ganz einfach: ein Name/Wert-Paar vom Typ „authToken=xyx“). Wenn das Token fehlt oder nicht auf „xyx“ festgelegt ist, wird der Statuscode „403 (Verboten)“ zurückgegeben.

Zunächst wandle ich die Abfragezeichenfolge durch Aufrufen der GetQueryNameValuePairs-Methode für das an die Methode übergebene HttpRequestMessage-Objekt in einen Satz von Name/Wert-Paaren um. Anschließend rufe ich mit LINQ das Token (oder bei fehlendem Token den Wert „null“) ab. Bei fehlendem oder ungültigem Token erstelle ich ein HttpResponseMessage-Objekt mit entsprechendem HTTP-Statuscode, binde es in ein TaskCompletionSource-Objekt ein und gebe es zurück:

string usingRegion = (from kvp in request.GetQueryNameValuePairs()
                      where kvp.Key == "authToken"
                      select kvp.Value).FirstOrDefault();
if (usingRegion == null || usingRegion != "xyx")
{
  HttpResponseMessage resp =
     new HttpResponseMessage(HttpStatusCode.Forbidden);
  TaskCompletionSource tsc =
     new TaskCompletionSource<HttpResponseMessage>();
  tsc.SetResult(resp);
  return tsc.Task;
}

Wenn das Token vorhanden und auf den richtigen Wert eingestellt ist, erstelle ich ein GenericPrincipal-Objekt und lege anhand dieses Objekts die CurrentPrincipal-Eigenschaft des Threads fest (damit dieser Nachrichtenhandler auch von IIS unterstützt wird, lege ich zusätzlich die HttpContext User-Eigenschaft fest, sofern das HttpContext-Objekt ungleich null ist):

Thread.CurrentPrincipal = new GenericPrincipal(
  Thread.CurrentPrincipal.Identity.Name, null);     
if (HttpContext.Current != null)
{
  HttpContext.Current.User = Thread.CurrentPrincipal;
}

Nachdem die Anforderung über das Token und den Identitätssatz authentifiziert wurde, ruft der Nachrichtenhandler zur weiteren Verarbeitung die Basismethode auf:

return base.SendAsync(request, cancellationToken);

Wenn Ihr Nachrichtenhandler für jeden Controller verwendet werden soll, können Sie ihn wie andere Nachrichtenhandler zur Verarbeitungspipeline der Web-API hinzufügen. Um die Verwendung Ihres Handlers allerdings auf bestimmte Routen zu beschränken, müssen Sie ihn über die MapHttpRoute-Methode hinzufügen. Instanziieren Sie zunächst Ihre Klasse und übergeben Sie sie als fünften Parameter an „MapHttpRoute“ (dieser Code erfordert eine Imports/using-Anweisung für „System.Web.Http“):

routes.MapHttpRoute(
  "ServiceDefault",
  "api/Customers/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler());

Statt die InnerHandler-Eigenschaft innerhalb von „DelegatingHandler“ einzustellen, können Sie sie im Rahmen der Routendefinition auf den Standardverteiler festlegen:

routes.MapHttpRoute(
  "ServiceDefault",
  "api/{controller}/{id}",
  new { id = RouteParameter.Optional },
  null,
  new PHVAuthorizingMessageHandler
  {InnerHandler = new HttpControllerDispatcher(
    GlobalConfiguration.Configuration)});

Nun wird die InnerHandler-Eigenschaft nicht auf mehrere „DelegatingHandlers“ verteilt, sodass Sie sie an einer einzelnen Stelle (nämlich dort, wo Sie Ihre Routen definieren) verwalten.

Erweitern des Prinzipals

Wenn eine Autorisierung von Anforderungen nach Name und Rolle nicht ausreicht, können Sie den Autorisierungsprozess durch Erstellung einer eigenen Prinzipalklasse erweitern. Implementieren Sie hierzu die IPrincipal-Schnittstelle. Um eine benutzerdefinierte Prinzipalklasse umfassend nutzen zu können, müssen Sie allerdings ein eigenes benutzerdefiniertes Autorisierungsattribut erstellen oder Ihren Dienstmethoden benutzerdefinierten Code hinzufügen.

Wenn Sie beispielsweise eine Reihe von Diensten betreiben, die nur für Benutzer aus einer bestimmten Region zugänglich sein sollen, können Sie eine einfache Prinzipalklasse erstellen, die die IPrincipal-Schnittstelle implementiert und der Klasse eine Region-Eigenschaft hinzufügt. Siehe hierzu Abbildung 2.

Abbildung 2 – Erstellen eines benutzerdefinierten Prinzipals mit zusätzlichen Eigenschaften

public class PHVPrincipal: IPrincipal
{
  public PHVPrincipal(string Name, string Region)
  {
    this.Name = Name;
    this.Region = Region;
  }
  public string Name { get; set; }
  public string Region { get; set; }
  public IIdentity Identity
  {
    get
    {
      return new GenericIdentity(this.Name);
    }
    set
    {
      this.Name = value.Name;
    }
   }
   public bool IsInRole(string role)
   {
     return true;
   }

Um diese neue Prinzipalklasse (die mit jedem Host funktioniert) nutzen zu können, müssen Sie sie lediglich instanziieren und anschließend die CurrentPrincipal- und die User-Eigenschaft festlegen. Mit dem folgenden Code wird nach einem Wert in der Abfragezeichenfolge der Anforderung gesucht, der mit dem Namen „region“ verknüpft ist. Der Wert wird abgerufen und anschließend zum Festlegen der Region-Eigenschaft des Prinzipals verwendet. Hierzu wird der Wert an den Konstruktor der Klasse übergeben:

string region = (from kvp in request.GetQueryNameValuePairs()
                 where kvp.Key == "region"
                 select kvp.Value).FirstOrDefault();
Thread.CurrentPrincipal = new PHVPrincipal(userName, region);

Wenn Sie Microsoft .NET Framework 4.5 verwenden, sollten Sie nicht die IPrincipal-Schnittstelle verwenden, sondern von der neuen ClaimsPrincipal-Klasse erben. „ClaimsPrincipal“ unterstützt sowohl die anspruchsbasierte Verarbeitung als auch die Integration in Windows Identity Foundation (WIF). Dieses Thema geht allerdings über den Rahmen des vorliegenden Artikels hinaus, weshalb ich es in einem nachfolgenden Artikel über anspruchsbasierte Sicherheit behandeln werde.

Autorisieren eines benutzerdefinierten Prinzipals

Wenn Sie ein neues Prinzipalobjekt eingerichtet haben, können Sie ein Autorisierungsattribut erstellen, das die neuen Daten nutzt, die der Prinzipal transportiert. Erstellen Sie als erstes eine Klasse, die von „System.Web.Http.AuthorizeAttri­bute“ erbt und deren IsAuthorized-Methode überschreibt (dieser Vorgang unterscheidet sich von ASP.NET MVC, wo Sie neue Authorization-Attribute durch Erweiterung von „System.Web.Http.Filters.Autho­rizationFilterAttribute“ erstellen). Die IsAuthorized-Methode wird an „HttpActionContext“ übergeben, dessen Eigenschaften als Teil Ihres Autorisierungsprozesses verwendet werden können. Allerdings müssen im vorliegenden Beispiel nur das Prinzipalobjekt aus der CurrentPrincipal-Eigenschaft des Threads extrahiert und in den benutzerdefinierten Prinzipaltyp umgewandelt und die Region-Eigenschaft überprüft werden. Bei einer erfolgreichen Autorisierung gibt der Code den Wert „true“ zurück. Bei fehlerhafter Autorisierung müssen Sie mit der ActionContext Response-Eigenschaft vor der Rückgabe des Werts „false“ eine benutzerdefinierte Antwort erstellen, wie in Abbildung 3 dargestellt.

Abbildung 3 – Filtern eines benutzerdefinierten Prinzipalobjekts

public class RegionAuthorizeAttribute : System.Web.Http.AuthorizeAttribute
{
  public string Region { get; set; }
  protected override bool IsAuthorized(HttpActionContext actionContext)
  {
    PHVPrincipal phvPcp = Thread.CurrentPrincipal as PHVPrincipal;
    if (phvPcp != null && phvPcp.Region == this.Region)
    {
      return true;
    }
    else
    {
      actionContext.Response =
        new HttpResponseMessage(
          System.Net.HttpStatusCode.Unauthorized)
        {
          ReasonPhrase = "Invalid region"
        };
      return false;
    }        
  }
}

Ihr benutzerdefinierter Autorisierungsfilter lässt sich genauso wie der Authorize-Standardfilter von ASP.NET verwenden. Da dieser Filter eine Region-Eigenschaft aufweist, muss die Eigenschaft auf die für diese Methode zulässige Region festgelegt werden (als Ergänzung einer Dienstmethode):

[RegionAuthorize(Region = "East")]
public HttpResponseMessage Get()
{

Für das vorliegende Beispiel habe ich mich dafür entschieden, von „Authorize­Attribute“ zu erben, da mein Autorisierungscode vollständig CPU-gebunden ist. Wenn für meinen Code der Zugriff auf eine Netzwerkressource (bzw. ein beliebiger E/A-Aufruf) erforderlich wäre, wäre es besser, die IAuthorization­Filter-Schnittstelle zu implementieren, da diese asynchrone Aufrufe unterstützt.

Wie bereits zu Beginn dieses Artikels erwähnt: Für ein typisches Web-API-Szenario ist, außer zum Schutz vor CSFR-Exploits, keine zusätzliche Autorisierung erforderlich. Wenn Sie Ihr Standardsicherheitssystem dennoch erweitern möchten, stellt die Web-API in der Verarbeitungspipeline verschiedene Optionen bereit, sodass Sie jede beliebige Sicherheitsstufe implementieren können. Denn es ist immer besser, mehrere Auswahlmöglichkeiten zu haben.

Peter Vogel ist Leiter der Firma PH&V Information Services und hat sich auf die ASP.NET-Entwicklung mit den Schwerpunkten serviceorientierte Architektur und XML sowie Datenbank- und Benutzeroberflächendesign spezialisiert.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Dominick Baier (thinktecture GmbH & Co KG), Barry Dorrans (Microsoft) und Mike Wasson (Microsoft)
Mike Wasson (mwasson@microsoft.com) ist Programmierer bei Microsoft. Er schreibt derzeit über ASP.NET, wobei sein Schwerpunkt auf der Web-API liegt.

Barry Dorrans (Barry.Dorrans@microsoft.com), Security Developer bei Microsoft, arbeitet mit dem Team für die Azure-Plattform zusammen. Er ist Autor von „Beginning ASP.NET Security“. Vor seiner Zeit bei Microsoft war er als Entwickler von MVP-Sicherheitslösungen tätig. Trotz allem verschreibt es sich regelmäßig bei dem Wort „Encryption“.

Dominick Baier (dominick.baier@thinktecture.com) ist Sicherheitsberater bei thinktecture (thinktecture.com). Sein Schwerpunktbereich liegt auf der Identitäten- und Zugriffsteuerung in verteilten Anwendungen. Er hat die bekannten Open-Source-Projekte „IdentityModel“ und „IdentityServer“ entwickelt. Sie finden seinen Blog unter leastprivilege.com.