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
.
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:
AddJwtBearer
is called.await wso2Actions.JwtOperations.GetTokenValidationParameters();
is called.GetTokenValidationParameters()
invokes an HttpClient
with await
. HttpClient
does an awaited call to get the public signing key of the issuer.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.
OnMessageReceived
event.HttpClient
gets the response with the public key.AddJwtBearer
continues.OnMessageReceived
event is setup.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:
AddJwtBearer
would be called at startup, not on the first call of the service.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
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
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With