JwtBearerEvents.OnMessageReceived no llamado para la primera invocación de operación

8

Estoy usando WSO2 como mi proveedor de identidad (IDP). Está poniendo el JWT en un encabezado llamado "X-JWT-Assertion".

Para alimentar esto al sistema ASP.NET Core, agregué un OnMessageReceivedevento. Esto me permite establecer tokenel valor proporcionado en el encabezado.

Aquí está el código que tengo que hacer eso (la parte clave son las últimas 3 líneas de código sin paréntesis):

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;
        }
    }
};

Todo esto funciona perfectamente, excepto la primera llamada después de que se inicia el servicio. Para ser claros, cada llamada, excepto la primera, funciona exactamente como yo quiero. (Pone el token y actualiza el Userobjeto como lo necesito).

Pero para la primera llamada, OnMessageReceivedno se golpea. Y el Userobjeto en mi controlador no está configurado.

Verifiqué HttpContextesa primera llamada y el encabezado "X-JWT-Assertion" está en la Request.Headerslista (con el JWT en él). Pero, por alguna razón, el OnMessageReceivedevento no se requiere.

¿Cómo puedo OnMessageReceivedser llamado para la primera invocación de una operación de servicio para mi servicio?

NOTA IMPORTANTE: descubrí que el problema estaba async awaiten AddJwtBearer. (Vea mi respuesta a continuación.) Eso es lo que realmente quería de esta pregunta.

Sin embargo, desde una recompensa no puede ser cancled, yo todavía otorgará la recompensa a cualquiera que pueda demostrar una manera de utilizar AddJwtBearercon async awaitdonde está a la espera de una verdadera HttpClientllamada. O muestre la documentación de por qué async awaitno se debe utilizar AddJwtBearer.

Vaccano
fuente
He puesto este controlador de eventos en una plantilla WebAPI de bolierplate; parece que lo seleccionó desde la primera solicitud. ¿puede ser que su orden de middleware lo esté afectando de alguna manera?
timur
1
@timur: vea mi actualización al final de mi pregunta. (Se debió a que async awaitno funcionaba bien con el
canal de
parece que AddJwtBearer(y subyacente AuthenticationBuilder.AddSchemeHelper) no espera llamadas asíncronas allí, solo agrega IConfigureOptions a los servicios. OnMessageReceived, por otro lado, se está esperando. Entonces, me pregunto si podría hacer que esa OnMessageReceivedlambda sea asíncrona, mover su llamada http al OnMessageReceivedcuerpo y de alguna manera almacenar los resultados de la caché allí.
timur

Respuestas:

6

ACTUALIZACIÓN:
La lambda es un Actionmétodo. No devuelve nada. Por lo tanto, tratar de hacer una asincronía no es posible sin que se dispare y se olvide.

Además, este método se invoca en la primera llamada. Entonces, la respuesta es llamar a todo lo que necesita en este método con anticipación y almacenarlo en caché. (Sin embargo, no he descubierto una forma no pirateada de usar elementos inyectados de dependencia para hacer esta llamada). Luego, durante la primera llamada, se llamará a esta lambda. En ese momento, debe extraer los valores que necesita de la memoria caché (por lo tanto, no ralentizará mucho la primera llamada).


Esto es lo que finalmente descubrí.

La lambda para AddJwtBearerno funciona async await. Mi llamada a await wso2Actions.JwtOperations.GetTokenValidationParameters();espera está bien, pero la línea de llamadas continúa sin esperar a AddJwtBearerque termine.

Con async awaitla orden de llamada va así:

  1. El servicio se inicia (y espera un momento para que todo sea feliz).
  2. Se realiza una llamada al servicio.
  3. AddJwtBearer se llama.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); se llama.
  5. GetTokenValidationParameters()invoca un HttpClientcon await.
  6. El HttpClienthace una llamada esperada para obtener la clave de firma pública del emisor.
  7. Mientras el HttpClientespera, el resto de la llamada original continúa. Aún no se han configurado eventos, por lo que simplemente continúa con la canalización de llamadas de forma normal.
    • Aquí es donde "parece omitir" el OnMessageReceivedevento.
  8. El HttpClientobtiene la respuesta con la clave pública.
  9. La ejecución de AddJwtBearercontinúa.
  10. El OnMessageReceivedevento está configurado.
  11. Se realiza una segunda llamada al servicio.
  12. Debido a que el evento finalmente se configuró, se llama al evento. ( AddJwtBearersolo se llama en la primera llamada).

Entonces, cuando ocurre la espera (en este caso, finalmente llega a una llamada HttpClient para obtener la Clave de firma del emisor), el resto de la primera llamada pasa. Debido a que aún no se configuró el evento, no se sabe llamar al controlador.

Cambié el lambda de AddJwtBearerno ser asíncrono y funcionó bien.

Notas:
Dos cosas parecen extrañas aquí:

  1. Pensé que AddJwtBearerse llamaría al inicio, no en la primera llamada del servicio.
  2. Pensé que eso AddJwtBearerno admitiría una asyncfirma lambda si no pudiera aplicar correctamente la espera.

No estoy seguro de si esto es un error o no, pero lo publiqué como uno solo por si acaso: https://github.com/dotnet/aspnetcore/issues/20799

Vaccano
fuente
Creaste la condición de carrera allí. Puede hacer el paso 10 antes del paso 4 para solucionar el problema. Ver mi respuesta :)
weichch
@weichch: desafortunadamente, se necesita la llamada esperada para poder decodificar el JWT. La primera llamada fallaría en la validación del token si ordenara la forma en que se muestra.
Vaccano
lo siento, mi mal, hubo otra condición de carrera que no conocía :) Intente actualizar. La respuesta original estaba esperando la misma tarea para cargar parámetros, que no debería ser la utilizada por OnMessageReceived.
weichch
0

Puede usar GetAwaiter().GetResult()para ejecutar código asíncrono en el inicio. Bloqueará el hilo, pero está bien porque solo se ejecuta una vez y está en el inicio de la aplicación.

Sin embargo, si no te gusta para bloquear el hilo y insiste en usar awaitpara obtener las opciones, se puede utilizar async awaiten Program.csconseguir sus opciones y almacenarlo en una clase estática y utilizarlo en el inicio.

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
fuente
0

La razón por la que sus primeras solicitudes de pareja no pueden activarse OnMessageReceivedno se debe al async voiddelegado que está utilizando, sino al orden de cómo se cargan los parámetros y los eventos que se adjuntan.

Adjunta los controladores a los eventos posteriores await , lo que significa que creó una condición de carrera aquí, que, por ejemplo, si llega una solicitud antes de que awaitse complete, no hay ningún controlador de eventos asociado OnMessageReceived.

Para solucionar esto, debe adjuntar controladores de eventos antes del primero await. Esto garantizará que siempre tenga asociados controladores de eventos OnMessageReceived.

Prueba este código:

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
fuente