Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JwtBearerEvents.OnMessageReceived not Called for First Operation Invocation

I am using WSO2 as my Identity Provider (IDP). It is putting the JWT in an header called "X-JWT-Assertion".

To feed this into the ASP.NET Core system, I added an OnMessageReceived event. This allows me to set the token to the value supplied in the header.

Here is the code that I have to do that (the key part is the last 3 lines of non-bracket code):

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

This all works perfectly except for the very first call after the service starts up. To be clear, every call, except for the first one works exactly as I want it to. (It puts the token in and updates the User object like I need.)

But for the first call, the OnMessageReceived is not hit. And the User object in my controller is not setup.

I checked HttpContext for that first call, and the "X-JWT-Assertion" header is in the Request.Headers list (with the JWT in it). But, for some reason, the OnMessageReceived event is not called for it.

How can I get OnMessageReceived to be called for the first invocation of a service operation for my service?

IMPORTANT NOTE: I figured out that the issue was async await in AddJwtBearer. (See my answer below.) That is what I really wanted out of this question.

However, since a bounty cannot be cancled, I will still award the bounty to anyone who can show a way to use AddJwtBearer with async await where it is awaiting an actual HttpClient call. Or show documentation of why async await is not supposed to be used with AddJwtBearer.

like image 674
Vaccano Avatar asked Apr 10 '20 17:04

Vaccano


2 Answers

UPDATE:
The lambda is an Action method. It does not return anything. So trying to do asynchrony in it is not possible without it being fire and forget.

Also, this method is invoked on the first call. So the answer is to call anything you need in this method in advance and cache it. (However, I have not figured out a non-hack way to use dependency injected items to make this call.) Then during the first call, this lambda will be called. At that time you should pull the values you need from cache (thus not slowing down the first call much).


This is what I finally figured out.

The lambda for AddJwtBearer does not work with async await. My call to await wso2Actions.JwtOperations.GetTokenValidationParameters(); awaits just fine, but the call pipeline goes on without waiting for AddJwtBearer to finish.

With async await the call order goes like this:

  1. The service starts up (and you wait a while for it all to be happy.)
  2. A call is made to the service.
  3. AddJwtBearer is called.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); is called.
  5. GetTokenValidationParameters() invokes an HttpClient with await.
  6. The HttpClient does an awaited call to get the public signing key of the issuer.
  7. While the HttpClient is awaiting, the rest of the original call goes through. No events had been setup yet, so it just goes on with the call pipeline as normal.
    • This is where it "appears to skip" the OnMessageReceived event.
  8. The HttpClient gets the response with the public key.
  9. Execution of AddJwtBearer continues.
  10. The OnMessageReceived event is setup.
  11. A second call is made to the service
  12. Because the event was eventually setup, the event is called. (AddJwtBearer is only called on the first call.)

So, when the await happens (in this case it eventually hits an HttpClient call to get the Issuer Signing Key), the rest of the first call goes through. Because there was no event setup yet, it does not know to call the handler.

I changed the lambda of AddJwtBearer to not be async and it worked just fine.

Notes:
Two things seem odd here:

  1. I would have thought that AddJwtBearer would be called at startup, not on the first call of the service.
  2. I would have thought that AddJwtBearer would not support an async lambda signature if it could not correctly apply the await.

I am not sure if this is a bug or not, but I posted it as one just in case: https://github.com/dotnet/aspnetcore/issues/20799

like image 72
Vaccano Avatar answered Nov 12 '22 16:11

Vaccano


The reason your first couple requests cannot trigger OnMessageReceived is not because of the async void delegate you are using, but the order of how the parameters being loaded and the events being attached.

You attach handlers to events after await, meaning you created a race condition here, that, if say some request arrives before the await is completed, there is no event handler attached to OnMessageReceived at all.

To fix this, you should attach event handlers before the first await. This will guarantee that you always have event handlers attached to OnMessageReceived.

Try this 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
    });
like image 3
weichch Avatar answered Nov 12 '22 18:11

weichch