JwtBearerEvents.OnMessageĐược nhận không được gọi cho lần gọi hoạt động đầu tiên


8

Tôi đang sử dụng WSO2 làm Nhà cung cấp nhận dạng (IDP). Nó đang đặt JWT trong một tiêu đề gọi là "Xác nhận X-JWT".

Để cung cấp điều này vào hệ thống ASP.NET Core, tôi đã thêm một OnMessageReceivedsự kiện. Điều này cho phép tôi đặt tokengiá trị được cung cấp trong tiêu đề.

Đây là mã mà tôi phải làm điều đó (phần quan trọng là 3 dòng mã không có khung cuối cùng):

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

Tất cả điều này hoạt động hoàn hảo ngoại trừ cuộc gọi đầu tiên sau khi dịch vụ khởi động. Để rõ ràng, mọi cuộc gọi, ngoại trừ cuộc gọi đầu tiên hoạt động chính xác như tôi muốn. (Nó đặt mã thông báo vào và cập nhật Userđối tượng như tôi cần.)

Nhưng đối với cuộc gọi đầu tiên, OnMessageReceivedkhông được nhấn. Và Userđối tượng trong bộ điều khiển của tôi không được thiết lập.

Tôi đã kiểm tra HttpContextcuộc gọi đầu tiên đó và tiêu đề "X-JWT-Ass Ass" nằm trong Request.Headersdanh sách (có JWT trong đó). Nhưng, vì một số lý do, OnMessageReceivedsự kiện này không được gọi cho nó.

Làm thế nào tôi có OnMessageReceivedthể được gọi cho lần gọi đầu tiên của một hoạt động dịch vụ cho dịch vụ của mình?

LƯU Ý QUAN TRỌNG: Tôi đã tìm ra rằng vấn đề là async awaittrong AddJwtBearer. (Xem câu trả lời của tôi dưới đây.) Đó là những gì tôi thực sự muốn từ câu hỏi này.

Tuy nhiên, vì tiền thưởng không thể bị hủy bỏ, tôi vẫn sẽ trao tiền thưởng cho bất kỳ ai có thể chỉ ra cách sử dụng AddJwtBearervới async awaitnơi đang chờ HttpClientcuộc gọi thực tế . Hoặc hiển thị tài liệu về lý do tại sao async awaitkhông được sử dụng với AddJwtBearer.


tôi đã đặt trình xử lý sự kiện này vào mẫu WebAPI của bolierplate - dường như chọn trình xử lý từ yêu cầu đầu tiên. có thể là thứ tự phần mềm trung gian của bạn bằng cách nào đó ảnh hưởng đến nó?
timur

1
@timur - Xem cập nhật của tôi ở cuối câu hỏi của tôi. (Đó là do async awaitkhông hoạt động tốt với đường ống cuộc gọi.)
Vaccano

có vẻ như AddJwtBearer(và bên dưới AuthenticationBuilder.AddSchemeHelper) không mong đợi các cuộc gọi async trong đó - nó chỉ thêm IConfigureOptions vào serices. OnMessageReceived, mặt khác - đang được chờ đợi trên. Vì vậy, tôi tự hỏi nếu bạn có khả năng có thể làm cho OnMessageReceivedlambda không đồng bộ, chuyển cuộc gọi http của bạn vào OnMessageReceivedcơ thể và bằng cách nào đó kết quả bộ nhớ cache trong đó?
timur

Câu trả lời:


6

CẬP NHẬT:
Lambda là một Actionphương pháp. Nó không trả lại bất cứ điều gì. Vì vậy, cố gắng để làm không đồng bộ trong nó là không thể mà không bị cháy và quên.

Ngoài ra, phương thức này được gọi trong cuộc gọi đầu tiên. Vì vậy, câu trả lời là gọi bất cứ điều gì bạn cần trong phương pháp này trước và lưu trữ nó. (Tuy nhiên, tôi chưa tìm ra cách không hack để sử dụng các mục được chèn phụ thuộc để thực hiện cuộc gọi này.) Sau đó, trong cuộc gọi đầu tiên, lambda này sẽ được gọi. Tại thời điểm đó, bạn nên kéo các giá trị bạn cần từ bộ đệm (do đó không làm chậm cuộc gọi đầu tiên nhiều).


Đây là những gì tôi cuối cùng đã tìm ra.

Lambda cho AddJwtBearerkhông làm việc với async await. Cuộc gọi của tôi await wso2Actions.JwtOperations.GetTokenValidationParameters();chờ đợi chỉ tốt, nhưng đường ống cuộc gọi vẫn tiếp tục mà không đợi AddJwtBearerkết thúc.

Với async awaitthứ tự cuộc gọi như thế này:

  1. Dịch vụ khởi động (và bạn đợi một lúc để tất cả được hạnh phúc.)
  2. Một cuộc gọi được thực hiện cho dịch vụ.
  3. AddJwtBearer được gọi là.
  4. await wso2Actions.JwtOperations.GetTokenValidationParameters(); được gọi là.
  5. GetTokenValidationParameters()gọi một HttpClientvới await.
  6. Cuộc HttpClientgọi được chờ đợi để lấy khóa ký công khai của tổ chức phát hành.
  7. Trong khi HttpClientchờ đợi, phần còn lại của cuộc gọi ban đầu đi qua. Chưa có sự kiện nào được thiết lập, vì vậy nó chỉ diễn ra với đường ống cuộc gọi như bình thường.
    • Đây là nơi nó "xuất hiện để bỏ qua" OnMessageReceivedsự kiện.
  8. Nhận HttpClientđược phản hồi với khóa công khai.
  9. Thi hành AddJwtBearertiếp tục.
  10. Sự OnMessageReceivedkiện được thiết lập.
  11. Một cuộc gọi thứ hai được thực hiện cho dịch vụ
  12. Bởi vì sự kiện cuối cùng đã được thiết lập, sự kiện này được gọi. ( AddJwtBearerchỉ được gọi trong cuộc gọi đầu tiên.)

Vì vậy, khi sự chờ đợi xảy ra (trong trường hợp này cuối cùng nó sẽ thực hiện cuộc gọi HttpClient để lấy Khóa ký của nhà phát hành), phần còn lại của cuộc gọi đầu tiên sẽ đi qua. Bởi vì chưa có thiết lập sự kiện nào, nên không biết gọi trình xử lý.

Tôi đã thay đổi lambda AddJwtBearerthành không đồng bộ và nó hoạt động tốt.

Ghi chú:
Hai điều có vẻ kỳ lạ ở đây:

  1. Tôi đã nghĩ rằng AddJwtBearersẽ được gọi khi khởi động, không phải trong cuộc gọi đầu tiên của dịch vụ.
  2. Tôi đã nghĩ rằng AddJwtBearersẽ không hỗ trợ asyncchữ ký lambda nếu nó không thể áp dụng chính xác sự chờ đợi.

Tôi không chắc đây có phải là lỗi hay không, nhưng tôi đã đăng nó dưới dạng chỉ trong trường hợp: https://github.com/dotnet/aspnetcore/issues/20799


Bạn đã tạo điều kiện cuộc đua ở đó. Bạn có thể làm bước 10 trước bước 4 để khắc phục sự cố. Xem câu trả lời của tôi :)
weichch

@weichch - thật không may, cuộc gọi được chờ đợi là cần thiết để có thể giải mã JWT. Cuộc gọi đầu tiên sẽ không xác thực mã thông báo nếu tôi yêu cầu cách bạn hiển thị.
Vaccano

Xin lỗi, điều tồi tệ của tôi là có một điều kiện cuộc đua khác mà tôi không biết :) Hãy thử cập nhật. Câu trả lời ban đầu đang chờ đợi trên cùng một nhiệm vụ để tải các tham số, không nên là câu hỏi đang được sử dụng OnMessageReceived.
weichch

0

Bạn có thể sử dụng GetAwaiter().GetResult()để thực thi mã async khi khởi động. Nó sẽ chặn luồng, nhưng không sao vì nó chỉ chạy một lần và nó khởi động ứng dụng.

Tuy nhiên nếu bạn không thích để ngăn chặn các chủ đề và nhấn mạnh vào sử dụng awaitđể có được các tùy chọn, bạn có thể sử dụng async awaittrong Program.csđể có được lựa chọn của bạn và lưu nó trong một lớp tĩnh và sử dụng nó trong khi khởi động.

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

0

Lý do cặp yêu cầu đầu tiên của bạn không thể kích hoạt OnMessageReceivedkhông phải vì async voidđại biểu bạn đang sử dụng, mà là thứ tự cách các tham số được tải và các sự kiện được đính kèm.

Bạn đính kèm các trình xử lý vào các sự kiện sau đó await , nghĩa là bạn đã tạo một điều kiện cuộc đua ở đây, rằng, nếu nói một số yêu cầu đến trước khi awaithoàn thành, không có trình xử lý sự kiện nào được đính kèm OnMessageReceived.

Để khắc phục điều này, bạn nên đính kèm các trình xử lý sự kiện trước lần đầu tiên await. Điều này sẽ đảm bảo rằng bạn luôn có các trình xử lý sự kiện kèm theo OnMessageReceived.

Hãy thử mã này:

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
    });
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.