JwtBearerEvents.OnMessageReceived nicht für den ersten Operationsaufruf aufgerufen

8

Ich verwende WSO2 als meinen Identity Provider (IDP). Es setzt das JWT in einen Header namens "X-JWT-Assertion".

Um dies in das ASP.NET Core-System einzuspeisen, habe ich ein OnMessageReceivedEreignis hinzugefügt . Dadurch kann ich tokenden Wert auf den im Header angegebenen Wert einstellen .

Hier ist der Code, den ich dazu machen muss (der Schlüsselteil sind die letzten 3 Zeilen des Codes ohne Klammern):

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(async options =>
{
    options.TokenValidationParameters = 
         await wso2Actions.JwtOperations.GetTokenValidationParameters();

    options.Events = new JwtBearerEvents()
    {
        // WSO2 sends the JWT in a different field than what is expected.
        // This allows us to feed it in.
        OnMessageReceived = context =>
        {
            context.Token = context.HttpContext.Request.Headers["X-JWT-Assertion"];
            return Task.CompletedTask;
        }
    }
};

Dies alles funktioniert perfekt, bis auf den ersten Anruf nach dem Start des Dienstes. Um klar zu sein, jeder Anruf, bis auf den ersten, funktioniert genau so, wie ich es möchte. (Es legt den Token ein und aktualisiert das UserObjekt wie benötigt.)

Aber beim ersten Anruf wird der OnMessageReceivednicht getroffen. Und das UserObjekt in meinem Controller ist nicht eingerichtet.

Ich habe HttpContextnach diesem ersten Aufruf gesucht und der Header "X-JWT-Assertion" befindet sich in der Request.HeadersListe (mit dem JWT darin). Aber aus irgendeinem Grund ist die OnMessageReceivedVeranstaltung nicht dafür vorgesehen.

Wie kann ich OnMessageReceivedzum ersten Aufruf eines Servicevorgangs für meinen Service aufgerufen werden?

WICHTIGER HINWEIS: Ich finde heraus , dass das Problem war async awaitin AddJwtBearer. (Siehe meine Antwort unten.) Das wollte ich wirklich von dieser Frage.

Da jedoch eine Prämie nicht cancled werden kann, werde ich vergeben noch die Prämie für jeden, der einen Weg zu verwenden , zeigen kann , AddJwtBearermit async awaitdenen es einen tatsächlichen wartet HttpClientAnruf. Oder zeigen Sie die Dokumentation, warum async awaitnicht mit verwendet werden soll AddJwtBearer.

Vaccano
quelle
Ich habe diesen Event-Handler in die Bolierplate-WebAPI-Vorlage eingefügt - scheint den Handler von der ersten Anfrage an abzuholen. Kann es sein, dass Ihre Middleware-Bestellung dies irgendwie beeinflusst?
Timur
1
@ Timur - Siehe mein Update am Ende meiner Frage. (Es lag async awaitdaran, dass die
Anrufpipeline
Es scheint, dass AddJwtBearer(und zugrunde liegende AuthenticationBuilder.AddSchemeHelper) keine asynchronen Aufrufe erwarten - es werden lediglich IConfigureOptions zu Serices hinzugefügt. OnMessageReceived, auf der anderen Seite - wird am erwartet. Ich frage mich also, ob Sie dieses OnMessageReceivedLambda möglicherweise asynchron machen, Ihren http-Aufruf in den OnMessageReceivedBody verschieben und dort irgendwie Cache-Ergebnisse erzielen könnten .
Timur

Antworten:

6

UPDATE:
Das Lambda ist eine ActionMethode. Es gibt nichts zurück. Der Versuch, Asynchronität darin zu machen, ist also nicht möglich, ohne dass es Feuer und Vergessen ist.

Diese Methode wird auch beim ersten Aufruf aufgerufen. Die Antwort ist also, alles, was Sie für diese Methode benötigen, im Voraus aufzurufen und zwischenzuspeichern. (Ich habe jedoch keine Nicht-Hack-Methode gefunden, um mit Abhängigkeiten injizierte Elemente für diesen Aufruf zu verwenden.) Während des ersten Aufrufs wird dieses Lambda aufgerufen. Zu diesem Zeitpunkt sollten Sie die benötigten Werte aus dem Cache ziehen (wodurch der erste Aufruf nicht wesentlich verlangsamt wird).


Das habe ich endlich herausgefunden.

Das Lambda für AddJwtBearerfunktioniert nicht mit async await. Mein Anruf await wso2Actions.JwtOperations.GetTokenValidationParameters();wartet in Ordnung, aber die Anrufpipeline wird fortgesetzt, ohne auf AddJwtBearerden Abschluss zu warten .

Mit async awaitder Anrufreihenfolge geht das so:

  1. Der Dienst wird gestartet (und Sie warten eine Weile, bis alles glücklich ist.)
  2. Der Dienst wird angerufen.
  3. AddJwtBearer wird genannt.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); wird genannt.
  5. GetTokenValidationParameters()ruft ein HttpClientmit auf await.
  6. Der HttpClientführt einen erwarteten Anruf durch, um den öffentlichen Signaturschlüssel des Emittenten zu erhalten.
  7. Während das HttpClientwartet, wird der Rest des ursprünglichen Anrufs durchlaufen. Es wurden noch keine Ereignisse eingerichtet, daher wird die Anrufpipeline wie gewohnt fortgesetzt.
    • Hier scheint das OnMessageReceivedEreignis zu "überspringen" .
  8. Der HttpClienterhält die Antwort mit dem öffentlichen Schlüssel.
  9. Ausführung von geht AddJwtBearerweiter.
  10. Das OnMessageReceivedEreignis ist eingerichtet.
  11. Ein zweiter Anruf wird an den Dienst getätigt
  12. Da das Ereignis schließlich eingerichtet wurde, wird das Ereignis aufgerufen. ( AddJwtBearerwird nur beim ersten Anruf aufgerufen.)

Wenn also das Warten stattfindet (in diesem Fall trifft es schließlich einen HttpClient-Aufruf, um den Issuer Signing Key zu erhalten), wird der Rest des ersten Aufrufs durchlaufen. Da noch kein Ereignis eingerichtet wurde, kann der Handler nicht aufgerufen werden.

Ich habe das Lambda so geändert AddJwtBearer, dass es nicht asynchron ist, und es hat gut funktioniert.

Anmerkungen:
Zwei Dinge scheinen hier seltsam:

  1. Ich hätte gedacht, dass AddJwtBearerdies beim Start aufgerufen wird, nicht beim ersten Aufruf des Dienstes.
  2. Ich hätte gedacht, dass AddJwtBearerdies eine asyncLambda-Signatur nicht unterstützen würde, wenn sie das Warten nicht korrekt anwenden könnte.

Ich bin nicht sicher, ob dies ein Fehler ist oder nicht, aber ich habe ihn nur für den Fall veröffentlicht: https://github.com/dotnet/aspnetcore/issues/20799

Vaccano
quelle
Sie haben dort die Rennbedingung erstellt. Sie können Schritt 10 vor Schritt 4 ausführen, um das Problem zu umgehen. Siehe meine Antwort :)
weichch
@weichch - Leider wird der erwartete Anruf benötigt, um das JWT entschlüsseln zu können. Der erste Aufruf würde die Token-Validierung fehlschlagen, wenn ich die von Ihnen angezeigte Bestellung bestellen würde.
Vaccano
Entschuldigung, mein schlechtes, es gab eine andere Rennbedingung, die mir nicht bekannt war :) Versuch aktualisiert. Die ursprüngliche Antwort wartete auf dieselbe Aufgabe zum Laden von Parametern, die nicht diejenige sein sollten, die von verwendet wird OnMessageReceived.
weichch
0

Sie können verwenden GetAwaiter().GetResult(), um beim Start asynchronen Code auszuführen. Es wird den Thread blockieren, aber es ist in Ordnung, da es nur einmal ausgeführt wird und sich beim Start der Anwendung befindet.

Wenn Sie den Thread jedoch nicht blockieren und darauf bestehen möchten await, die Optionen abzurufen, können Sie async awaitin verwenden Program.cs, um Ihre Optionen abzurufen, sie in einer statischen Klasse zu speichern und beim Start zu verwenden.

public class Program
{
    public static async Task Main(string[] args)
    {
        JwtParameter.TokenValidationParameters = await wso2Actions.JwtOperations.GetTokenValidationParameters();
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

public static class JwtParameter
{
    public static TokenValidationParameters TokenValidationParameters { get; set; }
}
Kahbazi
quelle
0

Der Grund, warum Ihre ersten paar Anfragen nicht ausgelöst werden können, OnMessageReceivedliegt nicht an dem von async voidIhnen verwendeten Delegaten, sondern an der Reihenfolge, in der die Parameter geladen und die Ereignisse angehängt werden.

Sie hängen Handler an Ereignisse nach an await , was bedeutet, dass Sie hier eine Race-Bedingung erstellt haben, dass, wenn beispielsweise eine Anforderung eintrifft, bevor der awaitabgeschlossen ist, überhaupt kein Event-Handler angehängt ist OnMessageReceived.

Um dies zu beheben, sollten Sie Ereignishandler vor dem ersten anhängen await. Dies garantiert, dass Sie immer Event-Handler haben OnMessageReceived.

Versuchen Sie diesen Code:

services.AddAuthentication(opt =>
    {
        // ...
    })
    .AddJwtBearer(async opt =>
    {
        var tcs = new TaskCompletionSource<object>();

        // Any code before the first await in this delegate can run
        // synchronously, so if you have events to attach for all requests
        // attach handlers before await.
        opt.Events = new JwtBearerEvents
        {
            // This method is first event in authentication pipeline
            // we have chance to wait until TokenValidationParameters
            // is loaded.
            OnMessageReceived = async context =>
            {
                // Wait until token validation parameters loaded.
                await tcs.Task;
            }
        };

        // This delegate returns if GetTokenValidationParametersAsync
        // does not complete synchronously 
        try
        {
            opt.TokenValidationParameters = await GetTokenValidationParametersAsync();
        }
        finally
        {
            tcs.TrySetResult(true);
        }

        // Any code here will be executed as continuation of
        // GetTokenValidationParametersAsync and may not 
        // be seen by first couple requests
    });
weichch
quelle