JwtBearerEvents.OnMessageReceived não chamado para primeira chamada de operação

8

Estou usando o WSO2 como meu provedor de identidade (IDP). Ele está colocando o JWT em um cabeçalho chamado "X-JWT-Assertion".

Para alimentar isso no sistema ASP.NET Core, adicionei um OnMessageReceivedevento. Isso permite que eu defina tokeno valor fornecido no cabeçalho.

Aqui está o código que eu tenho que fazer isso (a parte principal são as últimas 3 linhas do código que não está entre colchetes):

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

Tudo isso funciona perfeitamente, exceto na primeira chamada após o início do serviço. Para ser claro, todas as chamadas, exceto a primeira, funcionam exatamente como eu quero. (Ele coloca o token e atualiza o Userobjeto como eu preciso.)

Mas para a primeira chamada, o OnMessageReceivednão é atingido. E o Userobjeto no meu controlador não está configurado.

Eu verifiquei HttpContexta primeira chamada e o cabeçalho "X-JWT-Assertion" está na Request.Headerslista (com o JWT). Mas, por alguma razão, o OnMessageReceivedevento não é necessário.

Como posso OnMessageReceivedser chamado para a primeira chamada de uma operação de serviço para o meu serviço?

NOTA IMPORTANTE: eu descobri que o problema era async awaitno AddJwtBearer. (Veja minha resposta abaixo.) Era isso que eu realmente queria fora desta questão.

No entanto, como uma recompensa não pode ser excluída, ainda a concederei a qualquer pessoa que possa mostrar uma maneira de usá AddJwtBearer- async awaitla onde está aguardando uma HttpClientligação real . Ou mostre a documentação do motivo pelo qual async awaitnão deve ser usado AddJwtBearer.

Vaccano
fonte
Eu coloquei este manipulador de eventos no modelo WebAPI bolierplate - parece pegar o manipulador desde o primeiro pedido. pode ser que seu pedido de middleware esteja afetando de alguma forma?
timur 13/04
1
@timur - Veja minha atualização no final da minha pergunta. (Era devido a async awaitnão funcionar bem com o pipeline de chamadas.)
Vaccano
parece que AddJwtBearer(e subjacente AuthenticationBuilder.AddSchemeHelper) não esperam chamadas assíncronas por lá - apenas adiciona IConfigureOptions aos serviços. OnMessageReceived, por outro lado - está sendo aguardado em. Então, eu estou querendo saber se você poderia fazer esse OnMessageReceivedlambda assíncrono, mover sua chamada http para o OnMessageReceivedcorpo e, de alguma forma, armazenar os resultados lá?
timur 13/04

Respostas:

6

UPDATE:
O lambda é um Actionmétodo. Não retorna nada. Portanto, tentar fazer assincronia não é possível sem que seja fogo e esqueça.

Além disso, esse método é chamado na primeira chamada. Portanto, a resposta é chamar qualquer coisa que você precise neste método com antecedência e armazená-lo em cache. (No entanto, eu não descobri uma maneira não hackeada de usar itens injetados em dependência para fazer essa chamada.) Então, durante a primeira chamada, esse lambda será chamado. Nesse momento, você deve extrair os valores necessários do cache (não diminuindo muito a velocidade da primeira chamada).


Isto é o que eu finalmente descobri.

O lambda para AddJwtBearernão funciona com async await. Minha ligação await wso2Actions.JwtOperations.GetTokenValidationParameters();aguarda muito bem, mas o pipeline de chamadas continua sem esperar para AddJwtBearerterminar.

Com async awaita ordem de chamada, é assim:

  1. O serviço é iniciado (e você espera um pouco para que tudo seja feliz.)
  2. É feita uma chamada para o serviço.
  3. AddJwtBearer é chamado.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); é chamado.
  5. GetTokenValidationParameters()chama um HttpClientcom await.
  6. A HttpClientfaz uma chamada aguardada para obter a chave de assinatura pública do emitente.
  7. Enquanto o HttpClientaguarda, o restante da chamada original é finalizado. Nenhum evento foi configurado ainda, portanto, ele continua com o pipeline de chamadas normalmente.
    • É aqui que "parece pular" o OnMessageReceivedevento.
  8. O HttpClientobtém a resposta com a chave pública.
  9. Execução de AddJwtBearercontinua.
  10. O OnMessageReceivedevento está configurado.
  11. Uma segunda chamada é feita para o serviço
  12. Como o evento foi finalmente configurado, o evento é chamado. ( AddJwtBeareré chamado apenas na primeira chamada.)

Portanto, quando a espera acontece (nesse caso, eventualmente, uma ligação HttpClient para obter a chave de assinatura do emissor), o restante da primeira chamada é finalizada. Como ainda não havia configuração de evento, ele não sabe chamar o manipulador.

Mudei o lambda AddJwtBearerpara não ser assíncrono e funcionou muito bem.

Notas:
Duas coisas parecem estranhas aqui:

  1. Eu teria pensado que AddJwtBearerseria chamado na inicialização, não na primeira chamada do serviço.
  2. Eu teria pensado que AddJwtBearernão suportaria uma asyncassinatura lambda se não pudesse aplicar corretamente o aguardar.

Não tenho certeza se isso é um bug ou não, mas eu o publiquei como um caso: https://github.com/dotnet/aspnetcore/issues/20799

Vaccano
fonte
Você criou a condição de corrida lá. Você pode executar a etapa 10 antes da etapa 4 para solucionar o problema. Veja minha resposta :)
weichch 18/04
@weichch - infelizmente, a chamada esperada é necessária para poder decodificar o JWT. A primeira chamada falharia na validação do token se eu pedisse da maneira que você mostra.
Vaccano
desculpe meu mal, havia outra condição de corrida que eu não conhecia :) Tente atualizar. A resposta original estava aguardando a mesma tarefa para carregar parâmetros, que não deveriam ser os que estavam sendo usados OnMessageReceived.
weichch 19/04
0

Você pode usar GetAwaiter().GetResult()para executar código assíncrono na inicialização. Ele bloqueará o encadeamento, mas tudo bem, porque ele é executado apenas uma vez e está na inicialização do aplicativo.

No entanto, se você não gosta de bloquear a linha e insistir em usar awaitpara obter as opções, você pode usar async awaitem Program.csobter as suas opções e armazená-lo em uma classe estática e usá-lo na inicialização.

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

A razão pela qual suas primeiras solicitações não podem ser acionadas não OnMessageReceivedse deve ao async voiddelegado que você está usando, mas à ordem de como os parâmetros estão sendo carregados e os eventos estão sendo anexados.

Você anexa os manipuladores aos eventos depois await , o que significa que você criou uma condição de corrida aqui, que, se uma solicitação chegar antes da awaitconclusão, não há nenhum manipulador de eventos anexado OnMessageReceived.

Para corrigir isso, você deve anexar manipuladores de eventos antes do primeiro await. Isso garantirá que você sempre tenha manipuladores de eventos conectados OnMessageReceived.

Tente 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
fonte