Authentifizierungsfilter in ASP.NET-Web-API 2

von Mike Wasson

Ein Authentifizierungsfilter ist eine Komponente, die eine HTTP-Anforderung authentifiziert. Web-API 2 und MVC 5 unterstützen beide Authentifizierungsfilter, unterscheiden sich jedoch geringfügig, vor allem in den Benennungskonventionen für die Filterschnittstelle. In diesem Thema werden Web-API-Authentifizierungsfilter beschrieben.

Mit Authentifizierungsfiltern können Sie ein Authentifizierungsschema für einzelne Controller oder Aktionen festlegen. Auf diese Weise kann Ihre App verschiedene Authentifizierungsmechanismen für verschiedene HTTP-Ressourcen unterstützen.

In diesem Artikel zeige ich Code aus dem Beispiel für die Standardauthentifizierung auf https://github.com/aspnet/samples. Das Beispiel zeigt einen Authentifizierungsfilter, der das HTTP Basic Access Authentication-Schema (RFC 2617) implementiert. Der Filter wird in einer Klasse namens IdentityBasicAuthenticationAttributeimplementiert. Ich zeige nicht den gesamten Code aus dem Beispiel, nur die Teile, die veranschaulichen, wie ein Authentifizierungsfilter geschrieben wird.

Festlegen eines Authentifizierungsfilters

Wie andere Filter können Authentifizierungsfilter pro Controller, aktionsweise oder global auf alle Web-API-Controller angewendet werden.

Um einen Authentifizierungsfilter auf einen Controller anzuwenden, dekorieren Sie die Controllerklasse mit dem filter-Attribut. Der folgende Code legt den [IdentityBasicAuthentication] Filter für eine Controllerklasse fest, wodurch die Standardauthentifizierung für alle Aktionen des Controllers aktiviert wird.

[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }
    public IHttpActionResult Post() { . . . }
}

Um den Filter auf eine Aktion anzuwenden, dekorieren Sie die Aktion mit dem Filter. Der folgende Code legt den [IdentityBasicAuthentication] Filter für die -Methode des Post Controllers fest.

[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }

    [IdentityBasicAuthentication] // Enable Basic authentication for this action.
    public IHttpActionResult Post() { . . . }
}

Um den Filter auf alle Web-API-Controller anzuwenden, fügen Sie ihn GlobalConfiguration.Filters hinzu.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new IdentityBasicAuthenticationAttribute());

        // Other configuration code not shown...
    }
}

Implementieren eines Web-API-Authentifizierungsfilters

In der Web-API implementieren Authentifizierungsfilter die System.Web.Http.Filters.IAuthenticationFilter-Schnittstelle . Sie sollten auch von System.Attribute erben, um als Attribute angewendet zu werden.

Die IAuthenticationFilter-Schnittstelle verfügt über zwei Methoden:

  • AuthenticateAsync authentifiziert die Anforderung, indem Anmeldeinformationen in der Anforderung überprüft werden, sofern vorhanden.
  • ChallengeAsync fügt der HTTP-Antwort bei Bedarf eine Authentifizierungsanforderung hinzu.

Diese Methoden entsprechen dem in RFC 2612 und RFC 2617 definierten Authentifizierungsfluss:

  1. Der Client sendet Anmeldeinformationen im Autorisierungsheader. Dies geschieht in der Regel, nachdem der Client eine Antwort 401 (Nicht autorisiert) vom Server erhält. Ein Client kann jedoch Anmeldeinformationen mit jeder Anforderung senden, nicht nur nach dem Abrufen eines 401.
  2. Wenn der Server die Anmeldeinformationen nicht akzeptiert, wird die Antwort 401 (Nicht autorisiert) zurückgegeben. Die Antwort enthält einen Www-Authenticate-Header, der mindestens eine Herausforderung enthält. Jede Herausforderung gibt ein vom Server erkanntes Authentifizierungsschema an.

Der Server kann auch 401 von einer anonymen Anforderung zurückgeben. In der Tat wird der Authentifizierungsprozess in der Regel so initiiert:

  1. Der Client sendet eine anonyme Anforderung.
  2. Der Server gibt 401 zurück.
  3. Die Clients senden die Anforderung erneut mit Anmeldeinformationen.

Dieser Ablauf umfasst sowohl Authentifizierungs- als auch Autorisierungsschritte .

  • Die Authentifizierung beweist die Identität des Clients.
  • Die Autorisierung bestimmt, ob der Client auf eine bestimmte Ressource zugreifen kann.

In der Web-API behandeln Authentifizierungsfilter die Authentifizierung, aber keine Autorisierung. Die Autorisierung sollte über einen Autorisierungsfilter oder innerhalb der Controlleraktion erfolgen.

Hier sehen Sie den Flow in der Web-API 2-Pipeline:

  1. Vor dem Aufrufen einer Aktion erstellt die Web-API eine Liste der Authentifizierungsfilter für diese Aktion. Dies umfasst Filter mit Aktionsbereich, Controllerbereich und globalem Bereich.
  2. Die Web-API ruft AuthenticateAsync für jeden Filter in der Liste auf. Jeder Filter kann Anmeldeinformationen in der Anforderung überprüfen. Wenn ein Filter die Anmeldeinformationen erfolgreich überprüft, erstellt der Filter ein IPrincipal und fügt es an die Anforderung an. Ein Filter kann an dieser Stelle auch einen Fehler auslösen. In diesem Fall wird der Rest der Pipeline nicht ausgeführt.
  3. Wenn kein Fehler vorliegt, fließt die Anforderung durch den Rest der Pipeline.
  4. Schließlich ruft die Web-API die ChallengeAsync-Methode jedes Authentifizierungsfilters auf. Filter verwenden diese Methode, um der Antwort bei Bedarf eine Herausforderung hinzuzufügen. In der Regel (aber nicht immer) geschieht dies als Reaktion auf einen 401-Fehler.

Die folgenden Diagramme zeigen zwei mögliche Fälle. Im ersten authentifiziert der Authentifizierungsfilter die Anforderung erfolgreich, ein Autorisierungsfilter autorisiert die Anforderung, und die Controlleraktion gibt 200 (OK) zurück.

Diagramm der erfolgreichen Authentifizierung

Im zweiten Beispiel authentifiziert der Authentifizierungsfilter die Anforderung, aber der Autorisierungsfilter gibt 401 (Nicht autorisiert) zurück. In diesem Fall wird die Controlleraktion nicht aufgerufen. Der Authentifizierungsfilter fügt der Antwort einen Www-Authenticate-Header hinzu.

Diagramm der nicht autorisierten Authentifizierung

Andere Kombinationen sind möglich, z. B. wenn die Controlleraktion anonyme Anforderungen zulässt, verfügen Sie möglicherweise über einen Authentifizierungsfilter, aber keine Autorisierung.

Implementieren der AuthenticateAsync-Methode

Die AuthenticateAsync-Methode versucht, die Anforderung zu authentifizieren. Hier die Signatur der Methode:

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

Die AuthenticateAsync-Methode muss eine der folgenden Aktionen ausführen:

  1. Nichts (no-op).
  2. Erstellen Sie ein IPrincipal , und legen Sie es für die Anforderung fest.
  3. Legen Sie ein Fehlerergebnis fest.

Option (1) bedeutet, dass die Anforderung keine Anmeldeinformationen hatte, die der Filter versteht. Option (2) bedeutet, dass der Filter die Anforderung erfolgreich authentifiziert hat. Option (3) bedeutet, dass die Anforderung über ungültige Anmeldeinformationen (z. B. das falsche Kennwort) verfügt, wodurch eine Fehlerantwort ausgelöst wird.

Hier finden Sie eine allgemeine Gliederung für die Implementierung von AuthenticateAsync.

  1. Suchen Sie in der Anforderung nach Anmeldeinformationen.
  2. Wenn keine Anmeldeinformationen vorhanden sind, tun Sie nichts, und geben Sie zurück (no-op).
  3. Wenn Anmeldeinformationen vorhanden sind, aber der Filter das Authentifizierungsschema nicht erkennt, tun Sie nichts, und geben Sie zurück (no-op). Ein anderer Filter in der Pipeline kann das Schema verstehen.
  4. Wenn anmeldeinformationen vorhanden sind, die der Filter versteht, versuchen Sie, sie zu authentifizieren.
  5. Wenn die Anmeldeinformationen ungültig sind, geben Sie 401 zurück, indem Sie festlegen context.ErrorResult.
  6. Wenn die Anmeldeinformationen gültig sind, erstellen Sie ein IPrincipal , und legen Sie fest context.Principal.

Der folgende Code zeigt die AuthenticateAsync-Methode aus dem Beispiel "Standardauthentifizierung" . Die Kommentare geben jeden Schritt an. Der Code zeigt mehrere Arten von Fehlern an: Einen Autorisierungsheader ohne Anmeldeinformationen, falsch formatierte Anmeldeinformationen und einen fehlerhaften Benutzernamen/Kennwort.

public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
    // 1. Look for credentials in the request.
    HttpRequestMessage request = context.Request;
    AuthenticationHeaderValue authorization = request.Headers.Authorization;

    // 2. If there are no credentials, do nothing.
    if (authorization == null)
    {
        return;
    }

    // 3. If there are credentials but the filter does not recognize the 
    //    authentication scheme, do nothing.
    if (authorization.Scheme != "Basic")
    {
        return;
    }

    // 4. If there are credentials that the filter understands, try to validate them.
    // 5. If the credentials are bad, set the error result.
    if (String.IsNullOrEmpty(authorization.Parameter))
    {
        context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
        return;
    }

    Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
    if (userNameAndPassword == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
    }

    string userName = userNameAndPassword.Item1;
    string password = userNameAndPassword.Item2;

    IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
    if (principal == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
    }

    // 6. If the credentials are valid, set principal.
    else
    {
        context.Principal = principal;
    }

}

Festlegen eines Fehlerergebnisses

Wenn die Anmeldeinformationen ungültig sind, muss der Filter auf ein IHttpActionResult festgelegt context.ErrorResult werden, das eine Fehlerantwort erstellt. Weitere Informationen zu IHttpActionResult finden Sie unter Aktionsergebnisse in Web-API 2.

Das Beispiel "Standardauthentifizierung" enthält eine AuthenticationFailureResult Klasse, die für diesen Zweck geeignet ist.

public class AuthenticationFailureResult : IHttpActionResult
{
    public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
    {
        ReasonPhrase = reasonPhrase;
        Request = request;
    }

    public string ReasonPhrase { get; private set; }

    public HttpRequestMessage Request { get; private set; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute());
    }

    private HttpResponseMessage Execute()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        response.RequestMessage = Request;
        response.ReasonPhrase = ReasonPhrase;
        return response;
    }
}

Implementieren von ChallengeAsync

Der Zweck der ChallengeAsync-Methode besteht darin, der Antwort bei Bedarf Authentifizierungsanforderungen hinzuzufügen. Hier die Signatur der Methode:

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

Die -Methode wird für jeden Authentifizierungsfilter in der Anforderungspipeline aufgerufen.

Es ist wichtig zu verstehen, dass ChallengeAsync aufgerufen wird, bevor die HTTP-Antwort erstellt wird und möglicherweise sogar bevor die Controlleraktion ausgeführt wird. Wenn ChallengeAsync aufgerufen wird, context.Result enthält ein IHttpActionResult, das später zum Erstellen der HTTP-Antwort verwendet wird. Wenn ChallengeAsync aufgerufen wird, wissen Sie noch nichts über die HTTP-Antwort. Die ChallengeAsync-Methode sollte den ursprünglichen Wert von context.Result durch ein neues IHttpActionResult ersetzen. Dieses IHttpActionResult muss das ursprüngliche umschließen context.Result.

Diagramm von ChallengeAsync

Ich nenne das ursprüngliche IHttpActionResult das innere Ergebnis und das neue IHttpActionResult das äußere Ergebnis. Das äußere Ergebnis muss wie folgt aussehen:

  1. Rufen Sie das innere Ergebnis auf, um die HTTP-Antwort zu erstellen.
  2. Untersuchen Sie die Antwort.
  3. Fügen Sie der Antwort bei Bedarf eine Authentifizierungsanforderung hinzu.

Das folgende Beispiel stammt aus dem Beispiel für die Standardauthentifizierung. Es definiert ein IHttpActionResult für das äußere Ergebnis.

public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
    public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
    {
        Challenge = challenge;
        InnerResult = innerResult;
    }

    public AuthenticationHeaderValue Challenge { get; private set; }

    public IHttpActionResult InnerResult { get; private set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            // Only add one challenge per authentication scheme.
            if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
            {
                response.Headers.WwwAuthenticate.Add(Challenge);
            }
        }

        return response;
    }
}

Die InnerResult -Eigenschaft enthält das innere IHttpActionResult. Die Challenge -Eigenschaft stellt einen Www-Authentication-Header dar. Beachten Sie, dass ExecuteAsync zuerst aufruft InnerResult.ExecuteAsync , um die HTTP-Antwort zu erstellen, und fügt dann bei Bedarf die Herausforderung hinzu.

Überprüfen Sie den Antwortcode, bevor Sie die Herausforderung hinzufügen. Die meisten Authentifizierungsschemas fügen nur dann eine Herausforderung hinzu, wenn die Antwort 401 ist, wie hier gezeigt. Einige Authentifizierungsschemas stellen jedoch eine Herausforderung für eine Erfolgsantwort dar. Weitere Informationen finden Sie beispielsweise unter Negotiate (RFC 4559).

Angesichts der AddChallengeOnUnauthorizedResult -Klasse ist der tatsächliche Code in ChallengeAsync einfach. Sie erstellen einfach das Ergebnis und fügen es an an context.Result.

public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
    var challenge = new AuthenticationHeaderValue("Basic");
    context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
    return Task.FromResult(0);
}

Hinweis: Im Beispiel für die Standardauthentifizierung wird diese Logik ein wenig abstrahiert, indem sie in einer Erweiterungsmethode platziert wird.

Kombinieren von Authentifizierungsfiltern mit Host-Level-Authentifizierung

"Authentifizierung auf Hostebene" ist die Authentifizierung, die vom Host (z. B. IIS) durchgeführt wird, bevor die Anforderung das Web-API-Framework erreicht.

Häufig möchten Sie die Authentifizierung auf Hostebene für den Rest Ihrer Anwendung aktivieren, sie jedoch für Ihre Web-API-Controller deaktivieren. Ein typisches Szenario besteht beispielsweise darin, die Formularauthentifizierung auf Hostebene zu aktivieren, aber die tokenbasierte Authentifizierung für die Web-API zu verwenden.

Um die Authentifizierung auf Hostebene innerhalb der Web-API-Pipeline zu deaktivieren, rufen Sie config.SuppressHostPrincipal() Ihre Konfiguration auf. Dies bewirkt, dass die Web-API den IPrincipal aus jeder Anforderung entfernt, die in die Web-API-Pipeline gelangt. Effektiv wird die Authentifizierung der Anforderung "aufheben".

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SuppressHostPrincipal();

        // Other configuration code not shown...
    }
}

Zusätzliche Ressourcen

ASP.NET-Web-API Sicherheitsfilter (MSDN Magazine)