مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هفت دقیقه

ممکن است تعدادی از اکشن متدهای API طراحی شده، محافظت شده باشند. بنابراین OpenAPI Specification تولیدی نیز باید به همراه مستندات کافی در این مورد باشد تا استفاده کنندگان از آن بدانند چگونه باید با آن کار کنند. نگارش سوم OpenAPI Specification از اعتبارسنجی و احراز هویت مبتنی بر هدرها مانند basic و یا bearer، همچنین حالت کار با API Keys مانند هدرها، کوئری استرینگ‌ها و کوکی‌ها و یا حالت OAuth2 و OpenID Connect پشتیبانی می‌کند و این موارد ذیل خواص securitySchemes و security در OpenAPI Specification ظاهر می‌شوند:
"securitySchemes":
{ "basicAuth":
       {
             "type":"http",
             "description":"Input your username and password to access this API",
             "scheme":"basic"
       }
}
…
"security":[
  {"basicAuth":[]}
]
- خاصیت securitySchemes انواع حالت‌های اعتبارسنجی پشتیبانی شده را لیست می‌کند.
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمت‌های خاصی از آن، انجام می‌دهد.

در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار می‌کند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمه‌ی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل ساده‌تر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی می‌کنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمی‌کند.


افزودن Basic Authentication به API برنامه

برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace OpenAPISwaggerDoc.Web.Authentication
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Missing Authorization header"));
            }

            try
            {
                var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
                var username = credentials[0];
                var password = credentials[1];

                if (username == "DNT" && password == "123")
                {
                    var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
                    var identity = new ClaimsIdentity(claims, Scheme.Name);
                    var principal = new ClaimsPrincipal(identity);
                    var ticket = new AuthenticationTicket(principal, Scheme.Name);
                    return Task.FromResult(AuthenticateResult.Success(ticket));
                }
                return Task.FromResult(AuthenticateResult.Fail("Invalid username or password"));
            }
            catch
            {
                return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header"));
            }
        }
    }
}
کار این AuthenticationHandler سفارشی، با بازنویسی متد HandleAuthenticateAsync شروع می‌شود. در اینجا به دنبال هدر ویژه‌ای با کلید Authorization می‌گردد. این هدر باید به همراه نام کاربری و کلمه‌ی عبوری با حالت base64 encoded باشد. اگر این هدر وجود نداشت و یا مقدار هدر Authorization، با فرمتی که مدنظر ما است قابل decode و همچنین جداسازی نبود، شکست اعتبارسنجی اعلام می‌شود.
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آن‌را از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی می‌کنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمه‌ی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آن‌را به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت می‌دهیم.

مرحله‌ی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: "Basic")
                    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
در اینجا توسط متد services.AddAuthentication، این scheme جدید که نام رسمی آن Basic است، به همراه Handler آن، به برنامه معرفی می‌شود.
همچنین نیاز است میان‌افزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
   // ...
            app.UseStaticFiles();

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، می‌توان فیلتر Authorize را به صورت سراسری اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
     // ...
   
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new AuthorizeFilter());
        // ...


تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن

پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
   // ...

                setupAction.AddSecurityDefinition("basicAuth", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "basic",
                    Description = "Input your username and password to access this API"
                });
            });
در اینجا توسط متد setupAction.AddSecurityDefinition، ابتدا یک نام تعریف می‌شود (از این نام، در قسمت بعدی تنظیمات استفاده خواهد شد). پارامتر دوم آن همان SecurityScheme است که توضیح داده شد. برای حالت basic auth، نوع آن Http است و اسکیمای آن basic. باید دقت داشت که مقدار خاصیت Scheme در اینجا، حساس به بزرگی و کوچکی حروف است.

پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمه‌ی authorize اضافه شده‌است:


با کلیک بر روی آن، صفحه‌ی ورود نام کاربری و کلمه‌ی عبور ظاهر می‌شود:


اگر آن‌را تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمه‌ی try it out آن و سپس کلیک بر روی دکمه‌ی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت می‌کنیم:


- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status401Unauthorized));

- همچنین هرچند با کلیک بر روی دکمه‌ی Authorize در Swagger UI و ورود نام کاربری و کلمه‌ی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمی‌شود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
    // ...

                setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "basicAuth"
                            }
                        },
                        new List<string>()
                    }
                });
            });
در اینجا OpenApiSecurityRequirement یک دیکشنری از نوع <<Dictionary<OpenApiSecurityScheme, IList<string است که کلید آن از نوع OpenApiSecurityScheme تعریف می‌شود و باید آن‌را به نمونه‌ای که توسط setupAction.AddSecurityDefinition پیشتر اضافه کردیم، متصل کنیم. این اتصال توسط خاصیت Reference آن و Id ای که به نام تعریف شده‌ی توسط آن اشاره می‌کند، صورت می‌گیرد. مقدار این دیکشنری نیز لیستی از رشته‌ها می‌تواند باشد (مانند توکن‌ها و scopes در OpenID Connect) که در اینجا با یک لیست خالی مقدار دهی شده‌است.

پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر می‌کند:


در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحه‌ی دیالوگ ورود نام کاربری و کلمه‌ی عبوری که پیشتر با کلیک بر روی دکمه‌ی Authorize ظاهر شد، نمایش داده می‌شود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out  آن، برای مثال می‌توان به API محافظت شده‌ی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-06.zip
  • #
    ‫۵ سال و ۲ ماه قبل، چهارشنبه ۵ تیر ۱۳۹۸، ساعت ۰۲:۰۳
    یک نکته‌ی تکمیلی: نشان دادن لیست API‌ها در swagger فقط برای کاربرانی که لاگین کرده اند

    در هنگام توسعه‌ی پروژه شاید برای شما مهم باشد که لیست api‌های شما برای افرادی که لاگین نکرده‌اند، قابل مشاهده نباشد. برای این منظور ابتدا باید سه کتابخانه مربوط به swagger را نصب نمایید:
        <PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
        <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="4.0.1" />
        <PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="4.5.2" />
    سپس یک کلاس را همراه با دو اکستنشن متد برای کانفیگ swagger میسازیم :
        public static class ServiceCollectionExtensions
        {
            public static void AddCustomSwagger(this IServiceCollection services)
            {
                services.AddSwaggerGen(options =>
                {
                    options.EnableAnnotations();
                    options.DocumentFilter<AuthenticationDocumentFilter>();
                    options.SwaggerDoc("v1", new Info { Version = "v1", Title = "Test API" });
                });
            }
            public static void UseSwaggerAndUI(this IApplicationBuilder app)
            {
                app.UseSwagger();
                app.UseSwaggerUI(options =>
                {
                    options.DocExpansion(DocExpansion.None);
                    options.SwaggerEndpoint("/swagger/v1/swagger.json", "Test API Docs");
                });
            }
        }
    در متد AddSwaggerGen از DocumentFilter استفاده کرده‌ایم. با استفاده از Document FIlter‌ها میتوانید خروجی api‌ها را در swagger، توسعه دهید. DocumentFilter که از نوع جنریک است، یک کلاس را به عنوان تایپ قبول میکند که باید از اینترفیس IDocumentFilter ارث بری کرده باشد. اینترفیس IDocumentFilter حاوی یک متد Apply است که دارای دو ورودی از نوع SwaggerDocument  و DocumentFilterContext میباشد. کلاس SwaggerDocument  مستندات api‌ها را در اختیار شما قرار میدهد و میتوانید آنهارا تغییر دهید.
    سپس کلاس AuthenticationDocumentFilter را پیاده سازی میکنیم:
      public class AuthenticationDocumentFilter : IDocumentFilter
        {
            private readonly IHttpContextAccessor httpContextAccessor;
    
            public AuthenticationDocumentFilter(IHttpContextAccessor httpContextAccessor)
            {
                this.httpContextAccessor = httpContextAccessor;
            }
    
            public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
            {
                if (!httpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
                {
                    swaggerDoc.Definitions = new Dictionary<string, Schema>();
                    swaggerDoc.Paths = new Dictionary<string, PathItem>();
                }
            }
        }
    در کلاس AuthenticationDocumentFilter از IHttpContextAccessor برای دسترسی به هویت کاربر استفاده کرده ایم که بعدا باید در متد ConfigureService متد AddHttpContextAccessor را جهت دسترسی به IHttpContextAccessor فراخوانی کنیم. در ادامه اگر کاربر لاگین نکرده باشد، تمامی api‌ها پاک شده و در سمت کاربر هیچ api ای مشاهده نمیشود.
    در صورت نیاز میتوان مشخص کرد کدام نوع api هارا نشان ندهد؛ به عنوان مثال Post و Put را نشان ندهد :
            public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
            {
                if (!httpContextAccessor.HttpContext.User.Identity.IsAuthenticated)
                {
                    foreach (var item in swaggerDoc.Paths)
                    {
                        item.Value.Post = null;
                        item.Value.Put = null;
                    }
                }
            }
    در ادامه برای ثبت سرویس‌ها در کلاس StartUp 
        public void ConfigureServices(IServiceCollection services)
            {
                services.AddHttpContextAccessor();
                services.AddAuthorization();
                services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie(options=>
                {
                    options.AccessDeniedPath = "/Login";
                    options.Cookie.HttpOnly = true;
                    options.LoginPath = "/Login";
                    options.LogoutPath = "/Login";
                    options.ExpireTimeSpan = TimeSpan.FromDays(15);
                    options.SlidingExpiration = true;
                    options.Cookie.IsEssential = true;
                    options.ReturnUrlParameter = "returnUrl";
                });
                services.AddMvc();
                services.AddCustomSwagger();
            }
    و اضافه کردن میان افزار swagger :
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                app.UseAuthentication();
                app.UseSwaggerAndUI();
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                              name: "default",
                              template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
  • #
    ‫۵ سال قبل، چهارشنبه ۱۳ شهریور ۱۳۹۸، ساعت ۲۰:۲۵
    سلام
    اگر امکانش هست نحوه پیاده سازی با JWT رو هم توضیح بدین
    • #
      ‫۵ سال قبل، پنجشنبه ۱۴ شهریور ۱۳۹۸، ساعت ۰۰:۲۵
      برای JWT این تغییر را نیاز دارد (توکن دریافتی را پس از لاگین/تولید باید دستی وارد کنید):

      services.AddSwaggerGen(c =>
      {
          // ... 
          var security = new Dictionary<string, IEnumerable<string>>
          {
              {"Bearer", new string[] { }},
          }; 
          c.AddSecurityDefinition("Bearer", new ApiKeyScheme
          {
              Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
              Name = "Authorization",
              In = "header",
              Type = "apiKey"
          });
          c.AddSecurityRequirement(security);
      });
  • #
    ‫۴ سال قبل، چهارشنبه ۲۶ شهریور ۱۳۹۹، ساعت ۲۱:۴۳
    سلام، 
    چطور می‌توان خروجی  YAML از Swagger در Net Core. گرفت؟ 
    • #
      ‫۴ سال قبل، چهارشنبه ۲۶ شهریور ۱۳۹۹، ساعت ۲۲:۳۷
      این مورد مدنظر شماست؟
      https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json

      • #
        ‫۴ سال قبل، چهارشنبه ۲۶ شهریور ۱۳۹۹، ساعت ۲۲:۵۶
        سلام، 
        ممنون از پاسخ شما، اما منظورم YAML بود که اصلاح کردم، بصورت پیش فرض Swagger ، خروجی Json می‌ده.
    • #
      ‫۴ سال قبل، چهارشنبه ۲۶ شهریور ۱۳۹۹، ساعت ۲۳:۵۵
      نگارش 5.6 آن به صورت رسمی از YAML پشتیبانی می‌کند (Support for emitting Swagger / OpenAPI in yaml format). روش استفاده:
      app.UseSwaggerUI(x => { x.SwaggerEndpoint("/swagger/v1/swagger.yaml", "Zeipt Dashboard API"); });
  • #
    ‫۱ سال و ۵ ماه قبل، سه‌شنبه ۱۶ اسفند ۱۴۰۱، ساعت ۰۰:۳۱
    با سلام؛ چرا بعد از اجرای HandleAuthenticateAsync و در صورت Fail شدن تو response پیغام خطا رو نشون نمیده و فقط UnAuthorized برمی گردونه. آیا راهی وجود داره که بتونیم تو response body خطای سفارشی رو هم نشون بدیم؟
    • #
      ‫۱ سال و ۵ ماه قبل، سه‌شنبه ۱۶ اسفند ۱۴۰۱، ساعت ۰۲:۰۴
      خطای مدنظر را در response body درج کنید:
      await Context.Response.WriteAsync("This is why you can't log in");
      و یا متد HandleChallengeAsync مربوط به AuthenticationHandler را بازنویسی کنید.
      • #
        ‫۱ سال و ۵ ماه قبل، سه‌شنبه ۱۶ اسفند ۱۴۰۱، ساعت ۰۲:۲۱
        از هر دو روش استفاده کردم جواب نداد. از دات نت ورژن 6 استفاده کردم.
      • #
        ‫۱ سال و ۵ ماه قبل، سه‌شنبه ۱۶ اسفند ۱۴۰۱، ساعت ۰۲:۲۶
        HandleChallengeAsync  رو در حالت دیباگ تست کردم اصلا اجرا نمیشه!
        private readonly SignInKeysOption _signInKeyOptions;
        private string failReason;
        
        public CredentialAuthenticationHandler(
          IOptionsMonitor < CustomAuthenticationOptions > options,
          IOptionsMonitor < SignInKeysOption > signInKeyOptions,
          ILoggerFactory logger,
          UrlEncoder encoder,
          ISystemClock clock): base(options, logger, encoder, clock) {
          _signInKeyOptions = signInKeyOptions.CurrentValue ??
            throw new ArgumentNullException(nameof(signInKeyOptions));
        }
        
        protected override Task HandleChallengeAsync(AuthenticationProperties properties) {
          Response.StatusCode = 401;
        
          if (failReason != null) {
            Response.HttpContext.Features.Get < IHttpResponseFeature > () !.ReasonPhrase = failReason;
          }
        
          return Task.CompletedTask;
        }
        
        protected async override Task < AuthenticateResult > HandleAuthenticateAsync() {
          string authorizationHeader = Request.Headers["Authorization"];
          if (authorizationHeader == null) {
            Logger.LogWarning("Authorization key in header is null or empty");
            //string result;
            //Context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            //result = JsonConvert.SerializeObject(new { error = "Authorization key in header is null or empty" });
            //Context.Response.ContentType = "application/json";
            //await Context.Response.WriteAsync(result);
            failReason = "Authorization key in header is null or empty";
            return AuthenticateResult.Fail("request doesn't contains header");
        
            //return await Task.FromResult(AuthenticateResult.Fail("UnAuthenticate"));
          }
        }
        • #
          ‫۱ سال و ۵ ماه قبل، سه‌شنبه ۱۶ اسفند ۱۴۰۱، ساعت ۱۷:۵۵
          این روش برای نوشتن دلیل شکست عملیات در response body، با بازنویسی متد HandleChallengeAsync، آزمایش شده:
          using System.Security.Claims;
          using System.Text;
          using System.Text.Encodings.Web;
          using Microsoft.AspNetCore.Authentication;
          using Microsoft.AspNetCore.Mvc;
          using Microsoft.AspNetCore.Mvc.Abstractions;
          using Microsoft.AspNetCore.Mvc.Formatters;
          using Microsoft.AspNetCore.Mvc.Infrastructure;
          using Microsoft.Extensions.Options;
          
          namespace OpenAPISwaggerDoc.Web.Authentication;
          
          public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
          {
              private string _failReason;
          
              public BasicAuthenticationHandler(
                  IOptionsMonitor<AuthenticationSchemeOptions> options,
                  ILoggerFactory logger,
                  UrlEncoder encoder,
                  ISystemClock clock)
                  : base(options, logger, encoder, clock)
              {
              }
          
              protected override Task<AuthenticateResult> HandleAuthenticateAsync()
              {
                  if (!Request.Headers.ContainsKey("Authorization"))
                  {
                      _failReason = "Missing Authorization header";
                      return Task.FromResult(AuthenticateResult.Fail(_failReason));
                  }
          
                  try
                  {
                      var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                      var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
                      var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
                      var username = credentials[0];
                      var password = credentials[1];
          
                      if (string.Equals(username, "DNT", StringComparison.Ordinal) &&
                          string.Equals(password, "123", StringComparison.Ordinal))
                      {
                          var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
                          var identity = new ClaimsIdentity(claims, Scheme.Name);
                          var principal = new ClaimsPrincipal(identity);
                          var ticket = new AuthenticationTicket(principal, Scheme.Name);
                          return Task.FromResult(AuthenticateResult.Success(ticket));
                      }
          
                      _failReason = "Invalid username or password";
                      return Task.FromResult(AuthenticateResult.Fail(_failReason));
                  }
                  catch
                  {
                      _failReason = "Invalid Authorization header";
                      return Task.FromResult(AuthenticateResult.Fail(_failReason));
                  }
              }
          
              protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
              {
                  await base.HandleChallengeAsync(properties);
                  if (Response.StatusCode == StatusCodes.Status401Unauthorized &&
                      !string.IsNullOrWhiteSpace(_failReason))
                  {
                      Response.Headers.Add("WWW-Authenticate", _failReason);
                      Response.ContentType = "application/json";
                      await WriteProblemDetailsAsync(_failReason);
                  }
              }
          
              private Task WriteProblemDetailsAsync(string detail)
              {
                  var problemDetails = new ProblemDetails { Detail = detail, Status = Context.Response.StatusCode };
                  var result = new ObjectResult(problemDetails)
                               {
                                   ContentTypes = new MediaTypeCollection(),
                                   StatusCode = problemDetails.Status,
                                   DeclaredType = problemDetails.GetType(),
                               };
                  var executor = Context.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
                  var routeData = Context.GetRouteData() ?? new RouteData();
                  var actionContext = new ActionContext(Context, routeData, new ActionDescriptor());
                  return executor.ExecuteAsync(actionContext, result);
              }
          }