مطالب
رمزنگاری و رمزگشایی خودکار خواص مدل‌ها در ASP.NET Core
فرض کنید قصد دارید خاصیت Id مدل مورد استفاده‌ی در یک View را رمزنگاری کنید تا در سمت کلاینت به سادگی قابل تغییر نباشد. همچنین این Id زمانیکه به سمت سرور ارسال شد، به صورت خودکار رمزگشایی شود و بدون نیاز به تغییرات خاصی در کدهای متداول اکشن متدها، اطلاعات نهایی آن قابل استفاده باشند. برای این منظور در ASP.NET Core می‌توان یک Action Result رمزنگاری کننده و یک Model binder رمزگشایی کننده را طراحی کرد.


نیاز به علامتگذاری خواصی که باید رمزنگاری شوند

می‌خواهیم خاصیت یا خاصیت‌های مشخصی، از یک مدل را رمزنگاری شده به سمت کلاینت ارسال کنیم. به همین جهت ویژگی خالی زیر را به پروژه اضافه می‌کنیم تا از آن تنها جهت علامتگذاری این نوع خواص، استفاده کنیم:
using System;

namespace EncryptedModelBinder.Utils
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class EncryptedFieldAttribute : Attribute { }
}


رمزنگاری خودکار مدل خروجی از یک اکشن متد

در ادامه کدهای کامل یک ResultFilter را مشاهده می‌کنید که مدل ارسالی به سمت کلاینت را یافته و سپس خواصی از آن‌را که با ویژگی EncryptedField مزین شده‌اند، به صورت خودکار رمزنگاری می‌کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldResultFilter : ResultFilterAttribute
    {
        private readonly IProtectionProviderService _protectionProviderService;
        private readonly ILogger<EncryptedFieldResultFilter> _logger;
        private readonly ConcurrentDictionary<Type, bool> _modelsWithEncryptedFieldAttributes = new ConcurrentDictionary<Type, bool>();

        public EncryptedFieldResultFilter(
            IProtectionProviderService protectionProviderService,
            ILogger<EncryptedFieldResultFilter> logger)
        {
            _protectionProviderService = protectionProviderService;
            _logger = logger;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            var model = context.Result switch
            {
                PageResult pageResult => pageResult.Model, // For Razor pages
                ViewResult viewResult => viewResult.Model, // For MVC Views
                ObjectResult objectResult => objectResult.Value, // For Web API results
                _ => null
            };

            if (model is null)
            {
                return;
            }

            if (typeof(IEnumerable).IsAssignableFrom(model.GetType()))
            {
                foreach (var item in model as IEnumerable)
                {
                    encryptProperties(item);
                }
            }
            else
            {
                encryptProperties(model);
            }
        }

        private void encryptProperties(object model)
        {
            var modelType = model.GetType();
            if (_modelsWithEncryptedFieldAttributes.TryGetValue(modelType, out var hasEncryptedFieldAttribute)
                && !hasEncryptedFieldAttribute)
            {
                return;
            }

            foreach (var property in modelType.GetProperties())
            {
                var attribute = property.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
                if (attribute == null)
                {
                    continue;
                }

                hasEncryptedFieldAttribute = true;

                var value = property.GetValue(model);
                if (value is null)
                {
                    continue;
                }

                if (value.GetType() != typeof(string))
                {
                    _logger.LogWarning($"[EncryptedField] should be applied to `string` proprties, But type of `{property.DeclaringType}.{property.Name}` is `{property.PropertyType}`.");
                    continue;
                }

                var encryptedData = _protectionProviderService.Encrypt(value.ToString());
                property.SetValue(model, encryptedData);
            }

            _modelsWithEncryptedFieldAttributes.TryAdd(modelType, hasEncryptedFieldAttribute);
        }
    }
}
توضیحات:
- در اینجا برای رمزنگاری از IProtectionProviderService استفاده شده‌است که در بسته‌ی DNTCommon.Web.Core تعریف شده‌است. این سرویس در پشت صحنه از سیستم Data Protection استفاده می‌کند.
- سپس رخ‌داد OnResultExecuting، بازنویسی شده‌است تا بتوان به مدل ارسالی به سمت کلاینت، پیش از ارسال نهایی آن، دسترسی یافت.
- context.Result می‌تواند از نوع PageResult صفحات Razor باشد و یا از نوع ViewResult مدل‌های متداول Viewهای پروژه‌های MVC و یا از نوع ObjectResult که مرتبط است به پروژه‌های Web Api بدون هیچ نوع View سمت سروری. هر کدام از این نوع‌ها، دارای خاصیت مدل هستند که در اینجا قصد بررسی آن‌را داریم.
- پس از مشخص شدن شیء Model، اکنون حلقه‌ای را بر روی خواص آن تشکیل داده و خواصی را که دارای ویژگی EncryptedFieldAttribute هستند، یافته و آن‌ها را رمزنگاری می‌کنیم.

روش اعمال این فیلتر باید به صورت سراسری باشد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }
از این پس مدل‌های تمام خروجی‌های ارسالی به سمت کلاینت، بررسی شده و در صورت لزوم، خواص آن‌ها رمزنگاری می‌شود.


رمزگشایی خودکار مدل دریافتی از سمت کلاینت

تا اینجا موفق شدیم خواص ویژه‌ای از مدل‌ها را رمزنگاری کنیم. مرحله‌ی بعد، رمزگشایی خودکار این اطلاعات در سمت سرور است. به همین جهت نیاز داریم تا در سیستم Model Binding پیش‌فرض ASP.NET Core مداخله کرده و منطق سفارشی خود را تزریق کنیم. بنابراین در ابتدا یک IModelBinderProvider سفارشی را تهیه می‌کنیم تا در صورتیکه خاصیت جاری در حال بررسی توسط سیستم Model Binding دارای ویژگی EncryptedFieldAttribute بود، از EncryptedFieldModelBinder برای پردازش آن استفاده کند:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.IsComplexType)
            {
                return null;
            }

            var propName = context.Metadata.PropertyName;
            if (string.IsNullOrWhiteSpace(propName))
            {
                return null;
            }

            var propInfo = context.Metadata.ContainerType.GetProperty(propName);
            if (propInfo == null)
            {
                return null;
            }

            var attribute = propInfo.GetCustomAttributes(typeof(EncryptedFieldAttribute), false).FirstOrDefault();
            if (attribute == null)
            {
                return null;
            }

            return new BinderTypeModelBinder(typeof(EncryptedFieldModelBinder));
        }
    }
}
که این EncryptedFieldModelBinder به صورت زیر تعریف می‌شود:
namespace EncryptedModelBinder.Utils
{
    public class EncryptedFieldModelBinder : IModelBinder
    {
        private readonly IProtectionProviderService _protectionProviderService;

        public EncryptedFieldModelBinder(IProtectionProviderService protectionProviderService)
        {
            _protectionProviderService = protectionProviderService;
        }

        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var logger = bindingContext.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
            var fallbackBinder = new SimpleTypeModelBinder(bindingContext.ModelType, logger);
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == ValueProviderResult.None)
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            var valueAsString = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(valueAsString))
            {
                return fallbackBinder.BindModelAsync(bindingContext);
            }

            var decryptedResult = _protectionProviderService.Decrypt(valueAsString);
            bindingContext.Result = ModelBindingResult.Success(decryptedResult);
            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار ارسالی به سمت سرور به صورت یک رشته دریافت شده و سپس رمزگشایی می‌شود و بجای مقدار فعلی خاصیت، مورد استفاده قرار می‌گیرد. به این ترتیب دیگر نیازی به تغییر کدهای اکشن متدها برای رمزگشایی اطلاعات نیست.

پس از این تعاریف نیاز است EncryptedFieldModelBinderProvider را به صورت زیر به سیستم معرفی کرد:
namespace EncryptedModelBinder
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDNTCommonWeb();
            services.AddControllersWithViews(options =>
            {
                options.ModelBinderProviders.Insert(0, new EncryptedFieldModelBinderProvider());
                options.Filters.Add(typeof(EncryptedFieldResultFilter));
            });
        }


یک مثال

فرض کنید مدل‌های زیر تعریف شده‌اند:
namespace EncryptedModelBinder.Models
{
    public class ProductInputModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}

namespace EncryptedModelBinder.Models
{
    public class ProductViewModel
    {
        [EncryptedField]
        public string Id { get; set; }

        [EncryptedField]
        public int Price { get; set; }

        public string Name { get; set; }
    }
}
که بعضی از خواص آن‌ها با ویژگی EncryptedField مزین شده‌اند.
اکنون کنترلر زیر زمانیکه رندر شود، View متناظر با اکشن متد Index آن، یکسری لینک را به اکشن متد Details، جهت مشاهده‌ی جزئیات محصول، تولید می‌کند. همچنین اکشن متد Products آن هم فقط یک خروجی JSON را به همراه دارد:
namespace EncryptedModelBinder.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            var model = getProducts();
            return View(model);
        }

        public ActionResult<string> Details(ProductInputModel model)
        {
            return model.Id;
        }

        public ActionResult<List<ProductViewModel>> Products()
        {
            return getProducts();
        }

        private static List<ProductViewModel> getProducts()
        {
            return new List<ProductViewModel>
            {
                new ProductViewModel { Id = "1", Name = "Product 1"},
                new ProductViewModel { Id = "2", Name = "Product 2"},
                new ProductViewModel { Id = "3", Name = "Product 3"}
            };
        }
    }
}
کدهای View اکشن متد Index به صورت زیر است:
@model List<ProductViewModel>

<h3>Home</h3>

<ul>
    @foreach (var item in Model)
    {
        <li><a asp-action="Details" asp-route-id="@item.Id">@item.Name</a></li>
    }
</ul>
در ادامه اگر برنامه را اجرا کنیم، می‌توان مشاهده کرد که تمام asp-route-id‌ها که به خاصیت ویژه‌ی Id اشاره می‌کنند، به صورت خودکار رمزنگاری شده‌اند:


و اگر یکی از لینک‌ها را درخواست کنیم، خروجی model.Id، به صورت معمولی و رمزگشایی شده‌ای مشاهده می‌شود (این خروجی یک رشته‌است که هیچ ویژگی خاصی به آن اعمال نشده‌است. به همین جهت، اینبار این خروجی معمولی مشاهده می‌شود). هدف از اکشن متد Details، نمایش رمزگشایی خودکار اطلاعات است.


و یا اگر اکشن متدی که همانند اکشن متدهای Web API، فقط یک شیء JSON را باز می‌گرداند، فراخوانی کنیم نیز می‌توان به خروجی رمزنگاری شده‌ی زیر رسید:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EncryptedModelBinder.zip
مطالب
تنظیمات CORS در ASP.NET Core
برنامه‌های امروزی، ممکن است به چندین Web API مستقل، تبدیل شده و سپس برنامه‌هایی (Front-ends) جدای از آن‌ها برای کار با آن‌ها ایجاد شوند. بنابراین این وظیفه‌ی برنامه‌‌های Web API است که مطمئن شوند کلاینت‌ها قادر به تعامل با آن‌ها هستند. CORS استانداردی است که یک چنین امکانی را مهیا می‌کند.



CORS چیست؟

CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شده‌است. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده می‌شود. برای مدیریت یک چنین درخواست‌هایی، استانداردی به نام CORS طراحی شده‌است. می‌توان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آن‌ها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.


Origin چیست؟

سرآغاز یک درخواست از سه قسمت تشکیل می‌شود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS  یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیش‌فرض بودن، ذکر نمی‌شوند

بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص می‌کند. در اینجا به تمام منابعی که از این سرآغاز شروع می‌شوند و سه قسمت یاد شده‌ی آن‌ها یکی است، same-origin گفته می‌شود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آن‌ها Different-Origin گفته خواهد شد.


سیاست امنیتی Same-Origin چیست؟

سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شده‌است، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF می‌شود. این سیاست امنیتی بسیار محدود کننده‌است و برای مثال مانع این می‌شود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شده‌است. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آن‌ها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود می‌شود.


چگونه می‌توان از تنظیمات CORS، برای رفع محدودیت‌های سیاست‌های دسترسی Same-Origin استفاده کرد؟

استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کرده‌است تا توسط آن‌ها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال می‌کند. سپس سرور پاسخی را به همراه هدرهای ویژه‌ای که مشخص می‌کنند به چه منابعی و چگونه می‌توان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع می‌شوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آن‌ها به اشتراک بگذارد، مشخص می‌شوند. در حالت توسعه‌ی برنامه می‌توان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفاده‌ی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials


درخواست‌های ویژه‌ی Pre-Flight

در بسیاری از موارد، مرورگر زمانیکه تشخیص می‌دهد درخواست صادر شده‌ی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال می‌کند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمی‌کنند و در برابر درخواست‌های ویژه‌ای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت می‌کنند (صرفا درخواست‌های Same-Origin را پردازش می‌کنند). به همین جهت است که اگر به برگه‌ی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواست‌های cross-origin به نظر دوبار ارسال شده‌اند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه می‌شود یا خیر و آیا اینقدر قدیمی است که صرفا درخواست‌های Same-Origin را پردازش می‌کند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.



البته درخواست‌های pre-flight بیشتر در حالت‌های زیر توسط مرورگر صادر می‌شوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواست‌های از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.

بنابراین در برنامه‌های SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال می‌کنند (و Origin آن‌ها یکی نیست)، حتما شاهد درخواست‌های Pre-Flight نیز خواهید بود.


تنظیمات CORS در ASP.NET Core

اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوه‌ی انجام تنظیمات آن در برنامه‌های ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام می‌شوند:
public void ConfigureServices(IServiceCollection services) 
{ 
    // Add service and create Policy with options 
    services.AddCors(options => 
    { 
        options.AddPolicy("CorsPolicy", 
            builder => builder.AllowAnyOrigin() 
            .AllowAnyMethod() 
            .AllowAnyHeader() 
            .AllowCredentials() ); 
    }); 
     
    // Make sure MVC is added AFTER CORS 
    services.AddMvc(); 
}
ابتدا توسط متد AddCors، سرویس‌های مرتبط، به سیستم تزریق وابستگی‌های ASP.NET Core اضافه خواهند شد و سپس در قسمت تنظیمات آن می‌توان همان هدرهای Access-Control-Allow را که پیشتر در مورد آن‌ها بحث کردیم، تعریف و مقدار دهی کرد. برای مثال تنظیم AllowAnyOrigin، همان استفاده‌ی از مقدار * در هدر Access-Control-Allow-Origin تولیدی است. باید دقت داشت که تنظیمات این سرویس‌ها باید پیش از افزودن سرویس‌های MVC انجام شوند.
در اینجا CorsPolicy که به عنوان پارامتر مشخص شده‌است، نام این سیاست دسترسی سفارشی است و در قسمت‌های مختلف برنامه می‌توان ارجاعاتی را به این نام تعریف کرد.

و برای تعریف سرآغازی خاص و یا متدی مشخص، می‌توان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفاده‌ی از AllowAnyOrigin کار صحیحی نیست ):
services.AddCors(options =>
{
    options.AddPolicy("AllowOrigin",
            builder => builder.WithOrigins("http://localhost:55294")
                              .WithMethods("GET", "POST", "HEAD"));
});

پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام می‌شود:
public void Configure(IApplicationBuilder app) 
{ 
    // ... 
 
    app.UseCors("CorsPolicy"); 
     
    // ... 
     
    app.UseMvc(routes => 
    { 
        routes.MapRoute( 
            name: "default", 
            template: "{controller=Home}/{action=Index}/{id?}"); 
    }); 
}
در اینجا متد UseCors که میان‌افزار CORS را فعال می‌کند، باید پیش‌از میان‌افزار Mvc تعریف شود تا بتواند هدرها را اعمال کند. همچنین نامی که در اینجا به عنوان پارامتر آن استفاده شده، دقیقا به همان نامی که برای تنظیمات سیاست دسترسی درنظر گرفته شد، اشاره می‌کند.
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار می‌دهد. اگر می‌خواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، می‌توان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر می‌شود):
[EnableCors("CorsPolicy")]
public class MyApiController : Controller

و یا اگر می‌خواهید از app.UseCors سراسری استفاده کنید، اما آن‌را برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، می‌توان از فیلتر [DisableCors] استفاده کرد.

چند نکته:
- به نام رشته‌ای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میان‌افزار CORS، برای درخواست‌های same-origin نیز هدری را تولید نمی‌کند.


امکان تعریف سیاست‌های دسترسی بدون نام نیز وجود دارد

در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار می‌کنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقه‌ای به ذکر این نام ندارید، می‌توان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آن‌را ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services)
{
      services.AddCors();
      services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
 
    app.UseCors(policyBuilder =>
    {
        string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" };
 
        policyBuilder
            .AllowAnyHeader()
            .AllowAnyMethod()
            .WithOrigins(origins);
    });
 
    app.UseMvc();
}

روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddDefaultPolicy(
            builder =>
            {
            builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
            });
          options.AddPolicy("MyCORSPolicy",
            builder =>
            {
                builder.WithOrigins("http://localhost:49373")
                                    .AllowAnyHeader()
                                    .AllowAnyMethod();
            });
     });
    services.AddMvc().AddNewtonsoftJson();
}
در اینجا در متد AddCors می‌توان چندین AddPolicy نامدار را داشت، به همراه یک AddDefaultPolicy بدون نام. استفاده‌ی از سیاست نام‌دار تعریف شده، یا توسط متد سراسری app.UseCors("MyCORSPolicy") انجام می‌شود و یا توسط فیلتر [EnableCors("MyCORSPolicy")]. اما اگر فیلتر [EnableCors] را بدون ذکر نامی قید کنیم، از همان تنظیمات AddDefaultPolicy استفاده خواهد کرد.


خطاهای متداول حین کار با CORS

خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر

Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>' 
from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
برای رفع این مشکل کافی است متد AllowCredentials نیز قید شود:
services.AddCors(options =>
{
     options.AddPolicy("AllowAllOrigins",
         builder =>
          {
              builder
                 .AllowAnyOrigin()
                 .AllowAnyHeader()
                 .AllowAnyMethod()
                 .AllowCredentials();
          });
    });

تمام اعتبارسنجی‌ها و اطلاعات کوکی‌ها در سمت سرور قابل دسترسی نیستند

CORS به صورت پیش‌فرض اطلاعات کوکی‌ها را به دومینی دیگر ارسال نمی‌کند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکی‌ها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینت‌ها اشاره می‌کند.
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

//In jQuery:
$.ajax({
  type: 'get',
  url: 'http://www.example.com/home',
  xhrFields: {
    withCredentials: true
}

//ES6 fetch API:
fetch(
   'http://remote-url.com/users',
   {
     'PUT',
     headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
     },
     mode: 'cors',
     credentials: 'include',
     body: JSON.stringify(body)
   }
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((error) => console.error(error));

CORS حساس به حروف کوچک و بزرگ است

روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده می‌کنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")]
//is not the same as this:
[EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
اگر بجای * دقیقا origins را مشخص کردید، بخاطر داشته باشید که این تنظیم به کوچکی و بزرگی حروف حساس است و در صورت اشتباهی (مانند ذکر MyWebSite بجای mywebsite متداول با حروف کوچک)، درخواست‌های رسیده مسدود خواهند شد. همچنین همانطور که ذکر شد، بین HTTP و HTTPS نیز تفاوت وجود دارد و origin این‌ها یکی درنظر گرفته نمی‌شود و حتی اگر تمام حروف را هم کوچک وارد کرده باشید، باز هم تنظیمات جداگانه‌ای را نیاز خواهند داشت.


در حالت بروز خطای مدیریت نشده‌ای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمی‌کند
اطلاعات بیشتر 
مطالب
بررسی تغییرات Blazor 8x - قسمت دوازدهم - قالب جدید پیاده سازی اعتبارسنجی و احراز هویت - بخش دوم
در قسمت قبل، با نام‌هایی مانند IdentityRevalidatingAuthenticationStateProvider و PersistingRevalidatingAuthenticationStateProvider آشنا شدیم. در این قسمت جزئیات بیشتری از این کلاس‌ها را بررسی می‌کنیم.


نحوه‌ی پیاده سازی AuthenticationStateProvider در پروژه‌های Blazor Server 8x

در کدهای زیر، ساختار کلی کلاس AuthenticationStateProvider ارائه شده‌ی توسط قالب رسمی پروژه‌های Blazor Server به همراه مباحث اعتبارسنجی مبتنی بر ASP.NET Core Identity را مشاهده می‌کنید:
public class IdentityRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
     // ...
    }
}
کار این کلاس، پیاده سازی کلاس پایه‌ی RevalidatingServerAuthenticationStateProvider است. این کلاس پایه، چیزی نیست بجز یک کلاس پیاده سازی کننده‌ی AuthenticationStateProvider که در آن توسط حلقه‌ای، کار یک تایمر را پیاده سازی کرده‌اند که برای مثال در اینجا هر نیم ساعت یکبار، متد ValidateAuthenticationStateAsync را صدا می‌زند.
برای مثال در اینجا (یعنی کلاس بازنویسی کننده‌ی متد ValidateAuthenticationStateAsync که توسط تایمر کلاس پایه فراخوانی می‌شود) اعتبار security stamp کاربر جاری، هر نیم ساعت یکبار بررسی می‌شود. اگر فاقد اعتبار بود، کلاس پایه‌ی استفاده شده، سبب LogOut خودکار این کاربر می‌شود.


نحوه‌ی پیاده سازی AuthenticationStateProvider در پروژه‌های Blazor WASM 8x

دو نوع پروژه‌ی مبتنی بر وب‌اسمبلی را می‌توان در دات نت 8 ایجاد کرد: پروژه‌های حالت رندر Auto و پروژه‌های حالت رندر WASM
در هر دوی این‌ها، از کامپوننت ویژه‌ای به نام PersistentComponentState استفاده شده‌است که معرفی آن‌را در قسمت هشتم این سری مشاهده کردید. کار این کامپوننت در سمت سرور به صورت زیر است:
public class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
    public PersistingRevalidatingAuthenticationStateProvider(
        ILoggerFactory loggerFactory,
        IServiceScopeFactory scopeFactory,
        PersistentComponentState state,
        IOptions<IdentityOptions> options)
        : base(loggerFactory)
    {
     // ...
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
        AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
     // ...
    }

    private async Task OnPersistingAsync()
    {
     // ...
                _state.PersistAsJson(nameof(UserInfo), new UserInfo
                {
                    UserId = userId,
                    Email = email,
                });
     // ...
    }
}
همانطور که مشاهده می‌کنید، مهم‌ترین تفاوت آن با پروژه‌های Blazor Server، ذخیره سازی state به صورت JSON است که اینکار توسط کامپوننت PersistentComponentState انجام می‌شود و این Component-Stateهای مخفی حاصل از فراخوانی PersistAsJson، فقط یکبار توسط قسمت کلاینت، به صورت زیر خوانده می‌شوند:
public class PersistentAuthenticationStateProvider(PersistentComponentState persistentState) : AuthenticationStateProvider
{
    private static readonly Task<AuthenticationState> _unauthenticatedTask =
        Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        if (!persistentState.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
        {
            return _unauthenticatedTask;
        }

        Claim[] claims = [
            new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
            new Claim(ClaimTypes.Name, userInfo.Email),
            new Claim(ClaimTypes.Email, userInfo.Email) ];

        return Task.FromResult(
            new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
                authenticationType: nameof(PersistentAuthenticationStateProvider)))));
    }
}
در این کلاس سمت کلاینت و قرار گرفته‌ی در پروژه‌های WASM، نحوه‌ی پیاده سازی AuthenticationStateProvider را مشاهده می‌کنید که توسط آن و به کمک PersistentComponentState، کار خواندن اطلاعات UserInfo ای که پیشتر توسط state.PersistAsJson_ در سمت سرور در انتهای HTML صفحه ذخیره شده بود، انجام می‌‌شود.

بنابراین PersistentComponentState کار پرکردن اطلاعات یک کش مانند را در سمت سرور انجام داده و آن‌را به صورت سریالایز شده با قالب JSON، به انتهای HTML صفحه اضافه می‌کند. سپس زمانیکه کلاینت بارگذاری می‌شود، این اطلاعات را خوانده و استفاده می‌کند. یکبار از متد PersistAsJson آن در سمت سرور استفاده می‌شود، برای ذخیره سازی اطلاعات و یکبار از متد TryTakeFromJson آن در سمت کلاینت، برای خواندن اطلاعات.

یک نکته: پیاده سازی anti-forgery-token هم با استفاده از PersistentComponentState صورت گرفته‌است.
مطالب
پیاده سازی یک سیستم دسترسی Role Based در Web API و AngularJs - بخش اول
در این مجموعه مقالات قصد دارم یک روش را برای پیاده سازی سیستم Role Based سفارشی شده، به صورت پروژه محور، در اختیار شما دوستان قرار دهم. این مجموعه مقالات در هر دو بخش سرور و کلاینت، در قالب یک پروژه‌ی واقعی ارائه خواهد شد. در سمت سرور از Web API و سیستم Identity 2.1 و در سوی کلاینت از تکنولوژی قدرتمند AngularJs کمک گرفته شده‌است. در این مجموعه قصد دارم تا مراحل کلی و اصلی این سیستم دسترسی را تشریح کنم.

مقدمه ای بر سیستم مبتنی بر نقش کاربران

شاید برای شما هم پیش آمده باشد که برای وب سایت یا وب اپلیکیشن خود، به دنبال یک سیستم دسترسی بوده‌اید که بتوانید در آن نقش‌های گوناگونی را تعریف کنید و هر نقش شامل یک سری دسترسی خاص باشد و همچنین با اضافه شدن هر کاربر به این سیستم، بتوانید آنها را به یک نقش خاص انتساب دهید. برای اجرای این امر، روش‌های گوناگونی وجود دارد. یکی از این روش‌ها در سایت CodeProject توسط آقای Stefan Wloch تشریح شده است. در این مقاله کلیت و هدف یک سیستم Role Based، ساختار نقش‌ها و کاربران و دسترسی‌های آنها به خوبی بیان شده است.
 
و اما کمی درباره روشهای موجود حول توسعه یک سیستم مبتنی بر نقش (Role Based) صحبت کنیم. در ابتدا لازم است یک تعریف کلی از یک سیستم Role Based داشته باشیم:
یک سیستم مدیریت کاربر مبتنی بر نقش، سیستمی است که در آن نقش‌های گوناگونی تعریف می‌گردد. به گونه‌ای که هر نقش شامل یک سری دسترسی هایی است که به کاربر اجازه می‌دهد تا به بخشی از سیستم دسترسی داشته باشد. هر کاربر در این سیستم می‌تواند یک و یا چند نقش متفاوت داشته باشد و بر اساس آن نقش‌ها و دسترسی‌های تعریف شده، درون هر نقش به قسمتی از سیستم دسترسی داشته باشد.
بر اساس این تعریف ما در نهایت سه موجودیت نقش (Role)، دسترسی (Permission) و کاربر (User) را خواهیم داشت. در این سیستم دسترسی را اینگونه تعریف میکنیم:
دسترسی در یک سیستم، مجموعه‌ای از حوزه‌ها و یا ناحیه‌هایی است که در سیستم تعریف می‌شود. این حوزه‌ها می‌توانند شامل یک متد یا مجموعه‌ای از متدهای نوشته شده و یا فراتر از آن، شامل مجموعه‌ای از کنترلرها باشد که کاربر اجازه‌ی فراخوانی آنها را دارد.

بدیهی است کاربر برای ما موجودیتی مشخص است. یک کاربر ویژگی‌هایی مانند نام، نام کاربری، سن، رمز عبور و ... خاص خود را داراست که به واسطه این ویژگی‌ها از کاربری دیگر تمییز داده میشود. کاربر در سیستم طی دو مرحله جداگانه Authentication و Authorization مجاز است تا از بخش‌هایی از سیستم استفاده کند. مرحله Authentication به طور خلاصه شامل مرحله‌ای است که هویت کاربر (به عنوان مثال نام کاربری و رمز عبور) تایید صلاحیت میشود. این مرحله در واقع تایید کننده کاربر است و اما بخش بعدی که ما قصد داریم تا در این مورد راهکاری را ارائه دهیم بخش Authorization است. در این بخش به کاربر بر اساس نقش وی دسترسی‌هایی اعطا می‌گردد و کاربر را به استفاده از بخش‌هایی از سیستم مجاز میدارد.

دیاگرام زیر نمود سه موجودیت کاربر، نقش و دسترسی میباشد.

برای شرح دیاگرام فوق این چنین میتوان گفت که هر کاربر مجاز است چندین نقش داشته باشد و هر نقش نیز میتواند به چندین کاربر انتساب شود. در مورد دسترسی‌ها نیز به همین صورت، هر دسترسی نیز میتواند به چندین نقش انتساب شود.

ارائه یک سیستم مبتنی بر نقش کاربران با استفاده از تکنولوژی Web API و AngularJs

حال که از محتوا و عملکرد یک سیستم مبتنی بر نقش (Role Based) دانش کافی را کسب کردیم، قصد داریم تا به پیاده سازی کامل این سیستم بپردازیم. شاید برای شما سوال باشد که چرا ما قصد داریم تا این معماری را در Web API و AngularJs پیاده سازی کنیم. در جواب به این سوال باید بگوییم که پیاده سازی این روشها در تکنولوژی‌هایی مانند ASP.NET MVC و تکنولوژی‌ةای مشابه آن که صفحه را در سمت سرور می‌سازند، در بسیاری از منابع و مقالات اینترنتی موجود است. ما قصد داریم تا آنچه را که در یک Single Page Application پیاده سازی کرده‌ایم، به اشتراک بگذاریم. بنابراین قصد داریم تا علاوه بر تشریح کامل این سیستم که قسمت اعظم آن در سمت سرور انجام میشود، به تشریح نحوه تبادل اطلاعات و مدیریت دسترسی‌ها در سمت کلاینت نیز بپردازیم.
همانطور که میدانید ما در یک سیستم بر پایه SPA، برای ساخت یک صفحه ممکن است چندین API را فراخوانی کنیم. بنابراین APIهای نوشته شده به تعداد زیاد ولی با عملکرد اتمیک خواهند بود. به عنوان مثال در یک سیستم فروشگاه اینترنتی، برای ویرایش یک محصول، ممکن است دو یا چند متد API (فراخوانی آن محصول برای قرار دادن در فیلدهای ویرایش، ارسال اطلاعات پس از ویرایش به سرور و ...) فراخوانی گردند. با این حساب ما در تعریف و ایجاد دسترسی‌ها دو راه را خواهیم داشت.
  1. تعریف هر یک از متدهای اتمیک به عنوان یک مجوز دسترسی: در این روش نام کنترلر و نام متد به عنوان یک دسترسی تعریف خواهد شد. در این روش به ازای هر متد ما یک آیتم جدید را باید به جدول Permissions، اضافه نماییم و در نهایت برای تعریف یک نقش و انتساب دسترسی‌ها به یک کاربر، بایستی یک مجموعه چک باکس را که در نهایت به یک متد API ختم میشود، فعال یا غیر فعال کنیم.
  2. تعریف ناحیه‌های مختلف و کنترل‌های قابل انجام در آن ناحیه: در این روش ما قصد داریم تا مجموعه‌ای از متد‌ها را که هدف مشترکی را انجام خواهند داد، در یک ناحیه و یک کنترل بگنجانیم و از به وجود آمدن تعداد زیادی مجوز دسترسی جلوگیری نماییم. به عنوان مثال فرض کنید میخواهیم یک سطح دسترسی را با نام ویرایش کاربران تعریف کنیم. همانگونه که گفتیم ممکن است برای یک عملیات، دو و یا چندین متد درون یک کنترلر تعریف شوند. حال ما این متد‌ها را درون یک ناحیه دسترسی قرار خواهیم داد و آن را در یک حوزه و یک کنترل (Area & Control) میگنجانیم.
در بخش بعدی سعی میکنیم تا این مبحث را بازتر کنیم و به صورت عملی مراحل توسعه این مدل را تشریح نماییم.
مطالب
فعالسازی Windows Authentication در برنامه‌های ASP.NET Core 2.0
اعتبارسنجی مبتنی بر ویندوز، بر اساس قابلیت‌های توکار ویندوز و اختیارات اعطا شده‌ی به کاربر وارد شده‌ی به آن، کار می‌کند. عموما محل استفاده‌ی از آن، در اینترانت داخلی شرکت‌ها است که بر اساس وارد شدن افراد به دومین و اکتیودایرکتوری آن، مجوز استفاده‌ی از گروه‌های کاربری خاص و یا سطوح دسترسی خاصی را پیدا می‌کنند. میان‌افزار اعتبارسنجی ASP.NET Core، علاوه بر پشتیبانی از روش‌های اعتبارسنجی مبتنی بر کوکی‌‌ها و یا توکن‌ها، قابلیت استفاده‌ی از اطلاعات کاربر وارد شده‌ی به ویندوز را نیز جهت اعتبارسنجی او به همراه دارد.


فعالسازی Windows Authentication در IIS

پس از publish برنامه و رعایت مواردی که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بحث شد، باید به قسمت Authentication برنامه‌ی مدنظر، در کنسول مدیریتی IIS رجوع کرد:


و سپس Windows Authentication را با کلیک راست بر روی آن و انتخاب گزینه‌ی Enable، فعال نمود:


این تنظیم دقیقا معادل افزودن تنظیمات ذیل به فایل web.config برنامه است:
  <system.webServer>
    <security>
      <authentication>
        <anonymousAuthentication enabled="true" />
        <windowsAuthentication enabled="true" />
      </authentication>
    </security>
  </system.webServer>
اما اگر این تنظیمات را به فایل web.config اضافه کنید، پیام و خطای قفل بودن تغییرات مدخل windowsAuthentication را مشاهده خواهید کرد. به همین جهت بهترین راه تغییر آن، همان مراجعه‌ی مستقیم به کنسول مدیریتی IIS است.


فعالسازی Windows Authentication در IIS Express

اگر برای آزمایش می‌خواهید از IIS Express به همراه ویژوال استودیو استفاده کنید، نیاز است فایلی را به نام Properties\launchSettings.json با محتوای ذیل در ریشه‌ی پروژه‌ی خود ایجاد کنید (و یا تغییر دهید):
{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:3381/",
      "sslPort": 0
    }
  }
}
در اینجا الزامی به خاموش کردن anonymousAuthentication نیست. اگر برنامه‌ی شما قرار است هم توسط کاربران ویندوزی و هم توسط کاربران وارد شده‌ی از طریق اینترنت (و نه صرفا اینترانت داخلی) به برنامه، قابلیت دسترسی داشته باشد، نیاز است anonymousAuthentication به true تنظیم شده باشد (همانند تنظیمی که برای IIS اصلی ذکر شد).


تغییر مهم فایل web.config برنامه جهت هدایت اطلاعات ویندوز به آن

اگر پروژه‌ی شما فایل web.config ندارد، باید آن‌را اضافه کنید؛ با حداقل محتوای ذیل:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" 
            stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" 
            forwardWindowsAuthToken="true"/>
  </system.webServer>
</configuration>
که در آن خاصیت forwardWindowsAuthToken، حتما به true تنظیم شده باشد. این مورد است که کار اعتبارسنجی و یکی‌سازی اطلاعات کاربر وارد شده به ویندوز و ارسال آن‌را به میان‌افزار IIS برنامه‌ی ASP.NET Core انجام می‌دهد. بدون تنظیم آن، با مراجعه‌ی به سایت، شاهد نمایش صفحه‌ی login ویندوز خواهید بود.


تنظیمات برنامه‌ی ASP.NET Core جهت فعالسازی Windows Authentication

پس از فعالسازی windowsAuthentication در IIS و همچنین تنظیم forwardWindowsAuthToken به true در فایل web.config برنامه، اکنون جهت استفاده‌ی از windowsAuthentication دو روش وجود دارد:

الف) تنظیمات مخصوص برنامه‌های Self host
اگر برنامه‌ی وب شما قرار است به صورت self host ارائه شود (بدون استفاده از IIS)، تنها کافی است در تنظیمات ابتدای برنامه در فایل Program.cs، استفاده‌ی از میان‌افزار HttpSys را ذکر کنید:
namespace ASPNETCore2WindowsAuthentication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                                     .UseKestrel()
                                     .UseContentRoot(Directory.GetCurrentDirectory())
                                     .UseStartup<Startup>()
                                     .UseHttpSys(options => // Just for local tests without IIS, Or self-hosted scenarios on Windows ...
                                     {
                                         options.Authentication.Schemes =
                                              AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
                                         options.Authentication.AllowAnonymous = true;
                                         //options.UrlPrefixes.Add("http://+:80/");
                                     })
                                     .Build();
            host.Run();
        }
    }
}
در اینجا باید دقت داشت که استفاده‌ی از UseHttpSys با تنظیمات فوق، اعتبارسنجی یکپارچه‌ی با ویندوز را برای برنامه‌های self host خارج از IIS مهیا می‌کند. اگر قرار است برنامه‌ی شما در IIS هاست شود، نیازی به تنظیم فوق ندارید و کاملا اضافی است.


ب) تنظیمات مخصوص برنامه‌هایی که قرار است در IIS هاست شوند

در این‌حالت تنها کافی است UseIISIntegration در تنظیمات ابتدایی برنامه ذکر شود و همانطور که عنوان شد، نیازی به UseHttpSys در این حالت نیست:
namespace ASPNETCore2WindowsAuthentication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                                     .UseKestrel()
                                     .UseContentRoot(Directory.GetCurrentDirectory())
                                     .UseIISIntegration()
                                     .UseDefaultServiceProvider((context, options) =>
                                     {
                                         options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                                     })
                                     .UseStartup<Startup>()
                                     .Build();
            host.Run();
        }
    }
}


فعالسازی میان‌افزار اعتبارسنجی ASP.NET Core جهت یکپارچه شدن با Windows Authentication

در پایان تنظیمات فعالسازی Windows Authentication نیاز است به فایل Startup.cs برنامه مراجعه کرد و یکبار AddAuthentication را به همراه تنظیم ChallengeScheme آن به IISDefaults افزود:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.Configure<IISOptions>(options =>
            {
                // Sets the HttpContext.User
                // Note: Windows Authentication must also be enabled in IIS for this to work.
                options.AutomaticAuthentication = true;
                options.ForwardClientCertificate = true;
            });
            
            services.AddAuthentication(options =>
            {
                // for both windows and anonymous authentication
                options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
            });
        }
برای مثال اگر از ASP.NET Core Identity استفاده می‌کنید، سطر services.AddAuthentication فوق، پس از تنظیمات ابتدایی آن باید ذکر شود؛ هرچند روش فوق کاملا مستقل است از ASP.NET Core Identity و اطلاعات کاربر را از سیستم عامل و اکتیودایرکتوری (در صورت وجود) دریافت می‌کند.


آزمایش برنامه با تدارک یک کنترلر محافظت شده

در اینجا قصد داریم اطلاعات ذیل را توسط تعدادی اکشن متد، نمایش دهیم:
        private string authInfo()
        {
            var claims = new StringBuilder();
            if (User.Identity is ClaimsIdentity claimsIdentity)
            {
                claims.Append("Your claims: \n");
                foreach (var claim in claimsIdentity.Claims)
                {
                    claims.Append(claim.Type + ", ");
                    claims.Append(claim.Value + "\n");
                }
            }

            return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}";
        }
کار آن نمایش نام کاربر، وضعیت لاگین او و همچنین لیست تمام Claims متعلق به او می‌باشد:
namespace ASPNETCore2WindowsAuthentication.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [Authorize]
        public IActionResult Windows()
        {
            return Content(authInfo());
        }

        private string authInfo()
        {
            var claims = new StringBuilder();
            if (User.Identity is ClaimsIdentity claimsIdentity)
            {
                claims.Append("Your claims: \n");
                foreach (var claim in claimsIdentity.Claims)
                {
                    claims.Append(claim.Type + ", ");
                    claims.Append(claim.Value + "\n");
                }
            }

            return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}";
        }

        [AllowAnonymous]
        public IActionResult Anonymous()
        {
            return Content(authInfo());
        }

        [Authorize(Roles = "Domain Admins")]
        public IActionResult ForAdmins()
        {
            return Content(authInfo());
        }

        [Authorize(Roles = "Domain Users")]
        public IActionResult ForUsers()
        {
            return Content(authInfo());
        }
    }
}
برای آزمایش برنامه، ابتدا برنامه را توسط دستور ذیل publish می‌کنیم:
 dotnet publish
سپس تنظیمات مخصوص IIS را که در ابتدای بحث عنوان شد، بر روی پوشه‌ی bin\Debug\netcoreapp2.0\publish که محل قرارگیری پیش‌فرض خروجی برنامه است، اعمال می‌کنیم.
اکنون اگر برنامه را در مرورگر مشاهده کنیم، یک چنین خروجی قابل دریافت است:


در اینجا نام کاربر وارد شده‌ی به ویندوز و همچنین لیست تمام Claims او مشاهده می‌شوند. مسیر Home/Windows نیز توسط ویژگی Authorize محافظت شده‌است.
برای محدود کردن دسترسی کاربران به اکشن متدها، توسط گروه‌های دومین و اکتیودایرکتوری، می‌توان به نحو ذیل عمل کرد:
[Authorize(Roles = @"<domain>\<group>")]
//or
[Authorize(Roles = @"<domain>\<group1>,<domain>\<group2>")]
و یا می‌توان بر اساس این نقش‌ها، یک سیاست دسترسی جدید را تعریف کرد:
services.AddAuthorization(options =>
{
   options.AddPolicy("RequireWindowsGroupMembership", policy =>
   {
     policy.RequireAuthenticatedUser();
     policy.RequireRole(@"<domain>\<group>"));  
   }
});
و در آخر از این سیاست دسترسی استفاده نمود:
 [Authorize(Policy = "RequireWindowsGroupMembership")]
و یا با برنامه نویسی نیز می‌توان به صورت ذیل عمل کرد:
[HttpGet("[action]")]
public IActionResult SomeValue()
{
    if (!User.IsInRole(@"Domain\Group")) return StatusCode(403);
    return Ok("Some Value");
}


افزودن Claims سفارشی به Claims پیش‌فرض کاربر سیستم

همانطور که در شکل فوق ملاحظه می‌کنید، یک سری Claims حاصل از Windows Authentication در اینجا به شیء User اضافه شده‌اند؛ بدون اینکه برنامه، صفحه‌ی لاگینی داشته باشد و همینقدر که کاربر به ویندوز وارد شده‌است، می‌تواند از برنامه استفاده کند.
اگر نیاز باشد تا Claims خاصی به لیست Claims کاربر جاری اضافه شود، می‌توان از پیاده سازی یک IClaimsTransformation سفارشی استفاده کرد:
    public class ApplicationClaimsTransformation : IClaimsTransformation
    {
        private readonly ILogger<ApplicationClaimsTransformation> _logger;

        public ApplicationClaimsTransformation(ILogger<ApplicationClaimsTransformation> logger)
        {
            _logger = logger;
        }

        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            if (!(principal.Identity is ClaimsIdentity identity))
            {
                return Task.FromResult(principal);
            }

            var claims = addExistingUserClaims(identity);
            identity.AddClaims(claims);

            return Task.FromResult(principal);
        }

        private IEnumerable<Claim> addExistingUserClaims(IIdentity identity)
        {
            var claims = new List<Claim>();
            var user = @"VahidPC\Vahid";
            if (identity.Name != user)
            {
                _logger.LogError($"Couldn't find {identity.Name}.");
                return claims;
            }

            claims.Add(new Claim(ClaimTypes.GivenName, user));
            return claims;
        }
    }
و روش ثبت آن نیز در متد ConfigureServices فایل آغازین برنامه به صورت ذیل است:
            services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>();
            services.AddAuthentication(options =>
            {
                // for both windows and anonymous authentication
                options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
            });
هر زمانیکه کاربری به برنامه وارد شود و متد HttpContext.AuthenticateAsync فراخوانی گردد، متد TransformAsync به صورت خودکار اجرا می‌شود. در اینجا چون forwardWindowsAuthToken به true تنظیم شده‌است، میان‌افزار IIS کار فراخوانی HttpContext.AuthenticateAsync و مقدار دهی شیء User را به صورت خودکار انجام می‌دهد. بنابراین همینقدر که برنامه را اجرا کنیم، شاهد اضافه شدن یک Claim سفارشی جدید به نام ClaimTypes.GivenName که در متد addExistingUserClaims فوق آن‌را اضافه کردیم، خواهیم بود:


به این ترتیب می‌توان لیست Claims ثبت شده‌ی یک کاربر را در یک بانک اطلاعاتی استخراج و به لیست Claims فعلی آن افزود و دسترسی‌های بیشتری را به او اعطاء کرد (فراتر از دسترسی‌های پیش‌فرض سیستم عامل).

برای دسترسی به مقادیر این Claims نیز می‌توان به صورت ذیل عمل کرد:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var userName = User.FindFirstValue(ClaimTypes.Name);
var userName = User.FindFirstValue(ClaimTypes.GivenName);


کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید: ASPNETCore2WindowsAuthentication.zip
مطالب
میان‌افزار جدید Authorization در ASP.NET Core 3.0
در نگارش‌های اولیه‌ی ASP.NET Core، پشتیبانی از authorization، صرفا توسط ویژگی [Authorize]، قابل اعمال به اکشن متد خاصی بود. برای بهبود تنظیم این قابلیت، میان‌افزار جدید Authorization به ASP.NET Core 3.0 اضافه شده‌است و تنظیم آن جهت کار با امکانات امنیتی برنامه، الزامی است؛ در غیر اینصورت در حین مرور این صفحات و قسمت‌های محافظت شده، برنامه با خطای زیر متوقف خواهد شد:
Endpoint xyz contains authorization metadata, but a middleware was not found that supports authorization.
Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.


محل صحیح تعریف میان‌افزار Authorization در ASP.NET Core 3.0

توصیه شده‌است میان‌افزار جدید Authorization، با فراخوانی متد UseAuthorization، بلافاصله پس از فراخوانی متد UseAuthentication معرفی شود. در این حالت این میان‌افزار، با یک Policy پیش‌فرض تنظیم می‌شود که قابل تغییر و بازنویسی است:
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}
در مورد مفهوم متد MapDefaultControllerRoute، می‌توانید به نظرات تکمیلی مطلب Endpoint routing مراجعه کنید.
در ASP.NET Core 3.0، پس از تنظیمات فوق است که قطعه کد زیر و اعمال فیلتر Authorize، مجددا کار می‌کند:
public class HomeController : ControllerBase
{
    [Authorize]
    public IActionResult BuyWidgets()
    {
        ...
    }
}


روش تعریف فیلتر Authorize به صورت سراسری در ASP.NET Core 3.0

می‌توان AuthorizeFilter را به صورت یک فیلتر سراسری (global filter in MVC) تعریف کرد تا به تمام کنترلرها و اکشن متدها اعمال شود. هرچند این روش هنوز هم در ASP.NET Core 3.0 کار می‌کند، اما روش توصیه شده‌ی جدید آن به صورت زیر است:
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute().RequireAuthorization();
    });
}
در این حالت با اعمال RequireAuthorization به MapDefaultControllerRoute، سبب خواهیم شد تا اعتبارسنجی کاربر برای استفاده‌ی از تمام قسمت‌های برنامه، اجباری شود. در این بین اگر کنترلری و یا اکشن متدی نباید محافظت شود و دسترسی آزادانه‌ی به آ‌ن‌ها مدنظر است، می‌توان از فیلتر AllowAnonymous استفاده کرد:
[AllowAnonymous]
public class HomeController : ControllerBase
{
    ...
}


سفارشی سازی سیاست‌های Authorization در ASP.NET Core 3.0

اگر بخواهیم DefaultPolicy میان‌افزار Authorization را بازنویسی کنیم، می‌توان از متد services.AddAuthorization به صورت زیر استفاده کرد که در آن authentication و داشتن یک Scope خاص را اجباری می‌کند:
public void ConfigureServices(IServiceCollection services)
{
    ...
    
    services.AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .RequireScope("MyScope")
          .Build();
    });
}


تنظیم سیاست‌های دسترسی، زمانیکه هیچ نوع تنظیمی از پیش تعریف نشده‌است

با تنظیم FallbackPolicy، می‌توان تمام endpointهایی را که توسط فیلتر [Authorize] و یا ()RequireAuthorization محافظت نشده‌اند، محافظت کرد.
DefaultPolicy توسط فیلتر [Authorize] و یا ()RequireAuthorization مورد استفاده قرار می‌گیرد؛ اما FallbackPolicy زمانی بکار خواهد رفت که هیچ نوع سیاست دسترسی تنظیم نشده‌باشد. حالت پیش‌فرض FallbackPolicy، پذیرش تمام درخواست‌ها بدون اعتبارسنجی است.
کارکرد مثال زیر با مثال‌های قبلی که از DefaultPolicy استفاده می‌کردند، یکی است و هدف آن، اجبار به اعتبارسنجی کاربران در همه‌جا (تمام endpoints)، منهای مواردی است که از فیلتر AllowAnonymous استفاده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .RequireScope("MyScope")
          .Build();
    });
}

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

[AllowAnonymous]
public class HomeController : ControllerBase
{
}


امکان اعمال سفارشی میان‌افزار Authorization به Endpointهای مجزا

برای مثال در مبحث «بررسی سلامت برنامه»، فریم‌ورک، اعتبارسنجی درخواست‌های آن‌را پیش بینی نکرده‌است؛ اما اینبار می‌توان اعتبارسنجی را به endpoint آن اعمال کرد:
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints
            .MapHealthChecks("/healthz")
            .RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin", });
    });
}
همانطور که مشاهده می‌کنید، در اینجا RequireAuthorization، به صورت سفارشی به یک endpoint خاص اعمال شده‌است و تنظیمات آن، با تنظیمات سایر قسمت‌های برنامه یکی نیست.
مطالب
مستند سازی 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
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



تعریف موجودیت‌های مورد نیاز جهت طراحی یک سیستم اعتبارسنجی

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

جدول نقش‌های کاربران

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


تدارک لایه سرویس‌های برنامه

پس از مشخص شدن ساختار موجودیت‌ها و همچنین Context برنامه، اکنون می‌توان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

اگر خیر، این اعتبارسنجی رد شده و بلافاصله کوکی کاربر نیز معدوم خواهد شد.


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


پیاده سازی ورود و خروج به سیستم

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


آزمایش نهایی برنامه

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
Asp.Net Identity #1
API ، Identity جدید مایکروسافت جهت مدیریت کاربران در برنامه‌های ASP.NET می‌باشد. نقطه‌ی اتکای مایکروسافت در سالهای اخیر برای مدیریت کاربران سیستم ASP.NET Membership بود که از ضعف‌های طراحی رنج می‌برد. مهمترین محدودیت این سیستم این بود که داده‌های ذخیره شده توسط Schema، فقط قابلیت کار با SQL Server را دارا بود که توسعه‌ی آن بدون پیاده سازی دوباره‌ی کلاسهای تامین کننده ( Provider Classes ) بسیار مشکل بود. بعد از آن مایکروسافت جهت کاهش پیچیدگی سیستم ASEP.NET Membership، دو سیستم دیگر به نامهای Simple Membership و ASP.NET Universal Providers را عرضه نمود. این سیستم‌ها تا حدودی پیچیدگی را کم کردند، اما همچنان برمبنای SQL Server بودند. تا اینکه مایکروسافت سیستم ASP.NET Membership را با سیستم Identity جهت حل دو مشکل مطرح شده تعویض نمود. سیستم Identity هم پایدار است و هم قابل توسعه. به علاوه با استفاده از قابلیت Open Source بودن، قابل وفق دادن می‌باشد (یعنی برحسب نیاز پروژه می‌توان کل یا قسمتهایی را پیاده سازی نمود). برای بسیاری از توسعه دهندگان برنامه‌های ASP.NET، سیستم Identity اولین پرده برداری از  OWIN خواهد بود. در حقیقت OWIN یک لایه‌ی Abstract می‌باشد که برنامه‌ی وب را از محیطی که آن‌را میزبانی کرده، ایزوله می‌کند. هدف از ایزوله کردن، ایجاد انعطاف پذیری بیشتر در محیط هایی که برنامه‌های ASP.Net را میزبانی می‌کنند و همچنین ایجاد یک زیرساخت سبک وزن جهت سرویس دهی می‌باشد. همان طور که عنوان شد OWIN یک استاندارد باز است و مایکروسافت پروژه KATANA را به منظور پیاده سازی استانداردی از OWIN طراحی کرد. این طراحی همراه با یک سری کامپوننت به منظور تامین ویژگی‌هایی (Functionalities) که یک برنامه وب به آنها نیاز دارد همراه بود. زیبایی کار مایکروسافت در این بود که OWIN/KATANA پشته‌ی تکنولوژی ASP.NET را از بقیه‌ی قسمتهای .NET Framework ایزوله کرده که اجازه نرخ بالاتری از تغییرات را می‌دهد.
توسعه دهندگان OWIN به جای استفاده از کل پلتفرم (بر عکس چیزی که در حال حاضر در سرویسهای ASP.NET رخ می‌دهد) فقط سرویس‌هایی را که برنامه آنها نیاز دارد، انتخاب می‌کنند. این سرویسهای منفرد که در واژه نامه‌ی OWIN با نام Middleware شناخته می‌شوند، می‌توانند در نرخ‌های مختلفی توسعه داده شوند و توسعه دهندگان را قادر می‌سازند که بین تأمین کنندگان سرویسهای مختلف یکی را انتخاب کنند و فقط وابسته به سرویس‌های پیاده سازی شده توسط مایکروسافت نباشند.
و در پایان باید گفت که پلتفرم ASP.NET و IIS کنار گذاشته نخواهند شد. مایکروسافت شفاف سازی کرده‌است که یکی از ابعاد زیبای OWIN این است که به توسعه دهندگان اجازه انعطاف پذیری بیشتر را می‌دهد که در آن کامپوننت‌های Middleware بوسیله IIS میزبانی می‌شوند و هم اکنون پروژه‌ی KATANA توسط فضاهای نام System.Web پشتیبانی می‌شود. 
خوب دوستان این مقدمه ای بود بر سیستم Identity . انشالله در نوشته‌های بعدی بیشتر سیستم Identity را تحلیل و بررسی خواهیم کرد.