امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهارده دقیقه

در «قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی»، اطلاعات TestUser تنظیم شده‌ی در کلاس Config برنامه‌ی IDP را به بانک اطلاعاتی منتقل کردیم که در نتیجه‌ی آن سه جدول Users، UserClaims و UserLogins، تشکیل شدند. در اینجا می‌خواهیم سایر قسمت‌های کلاس Config را نیز به بانک اطلاعاتی منتقل کنیم.


تنظیم مجوز امضای توکن‌های IDP

namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
تا اینجا تنظیمات کلاس آغازین برنامه چنین شکلی را دارد که AddCustomUserStore آن‌را در قسمت دهم به آن افزودیم.
مرحله‌ی بعد، تغییر AddDeveloperSigningCredential به یک نمونه‌ی واقعی است. استفاده‌ی از روش فعلی آن چنین مشکلاتی را ایجاد می‌کند:
- اگر برنامه‌ی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا می‌کند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست می‌کند. یعنی با ری‌استارت شدن Application pool، کلیدهای مجوز امضای توکن‌ها تغییر می‌کنند که در نهایت سبب شکست اعتبارسنجی توکن‌های صادر شده‌ی توسط IDP می‌شوند.

بنابراین برای انتشار نهایی برنامه نمی‌توان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامه‌ی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده می‌کنیم. این فایل را از پوشه‌ی src\IDP\DNT.IDP\MakeCert نیز می‌توانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
 makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
در اینجا تاریخ شروع و پایان اعتبار مجوز ذکر شده‌اند. همچنین نتیجه‌ی آن به صورت خودکار در LocalMachine certificate store ذخیره می‌شود. به همین جهت اجرای آن نیاز به دسترسی admin را دارد.
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینه‌ی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحه‌ی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون می‌توانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:


در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.

چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفته‌است که باید این انتقال را انجام داد. در غیراینصورت می‌توان توسط آن توکن‌های صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینه‌ی All tasks -> export را انتخاب کنید. نکته‌ی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینه‌ی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمی‌کند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینه‌ی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.

پس از ایجاد مجوز امضای توکن‌ها و انتقال آن به Trusted Root Certification Authorities، نحوه‌ی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
        }

        private X509Certificate2 loadCertificateFromStore()
        {
            var thumbPrint = Configuration["CertificateThumbPrint"];
            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
                if (certCollection.Count == 0)
                {
                    throw new Exception("The specified certificate wasn't found.");
                }
                return certCollection[0];
            }
        }

        private X509Certificate2 loadCertificateFromFile()
        {
            // NOTE:
            // You should check out the identity of your application pool and make sure
            // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.
            var certificate = new X509Certificate2(
                fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", 
                    Configuration["X509Certificate:FileName"]),
                password: Configuration["X509Certificate:Password"],
                keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
                                 X509KeyStorageFlags.Exportable);
            return certificate;
        }
    }
}
متد کمکی loadCertificateFromStore، بر اساس thumbPrint مجوز تولید شده، آن‌را بارگذاری می‌کند. سپس این مجوز، توسط متد AddSigningCredential به IdentityServer معرفی خواهد شد و یا اگر فایل pfx ای را دارید، می‌توانید از متد loadCertificateFromFile استفاده کنید. این متد برای اینکه در IIS به درستی کار کند، نیاز است در خواص Application pool سایت IDP، گزینه‌ی Load user profile را انتخاب کرده باشید (مهم!).

پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آن‌را در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration
https://localhost:6001/.well-known/openid-configuration/jwks


انتقال سایر قسمت‌های فایل Config برنامه‌ی IDP به بانک اطلاعاتی

قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر داده‌های فایل Config، مانند Resources و Clients برنامه‌ی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامه‌های متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آن‌ها داخل فایل Config برنامه‌ی IDP، مشکلی را ایجاد نمی‌کند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامه‌ی IDP توسط یک صفحه‌ی مدیریتی داشته باشیم، نیاز است آن‌ها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر می‌کند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام این‌ها اکنون در حافظه ذخیره می‌شوند که با ری‌استارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفاده‌ی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمده‌ی این کار توسط خود تیم IdentityServer توسط بسته‌ی IdentityServer4.EntityFramework انجام شده‌است که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بسته‌ی نیوگت را مستقیما مورد استفاده قرار نمی‌دهیم. از این جهت که نیاز به 2 رشته‌ی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شده‌است و ترجیح می‌دهیم که اطلاعات آن‌را با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آن‌را از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases

انتقال موجودیت‌ها به پروژه‌ی DNT.IDP.DomainClasses

در این بسته‌ی دریافتی، در پوشه‌ی src\IdentityServer4.EntityFramework\Entities آن، کلاس‌های تعاریف موجودیت‌های متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایل‌ها را از این پروژه استخراج کرده و به پروژه‌ی DNT.IDP.DomainClasses در پوشه‌ی جدید IdentityServer4Entities اضافه می‌کنیم.
البته در این حالت پروژه‌ی DNT.IDP.DomainClasses نیاز به این وابستگی‌ها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" />
    <PackageReference Include="IdentityServer4" Version="2.2.0" />
  </ItemGroup>
</Project>
پس از این انتقال، فضاهای نام این کلاس‌ها را نیز اصلاح می‌کنیم؛ تا با پروژه‌ی جاری تطابق پیدا کنند.


انتقال تنظیمات روابط بین موجودیت‌ها، به پروژه‌ی DNT.IDP.DataLayer

در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بسته‌ی دریافتی، تعاریف تنظیمات این موجودیت‌ها به همراه نحوه‌ی برقراری ارتباطات بین آن‌ها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژه‌ی DNT.IDP.DataLayer اضافه می‌کنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کرده‌ایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایل‌های IEntityTypeConfiguration را معرفی می‌کنیم.


تعاریف DbSetهای متناظر با موجودیت‌های منتقل و تنظیم شده در پروژه‌ی DNT.IDP.DataLayer

پس از انتقال موجودیت‌ها و روابط بین آن‌ها، دو فایل DbContext را در این بسته‌ی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیت‌های تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار می‌دهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیت‌های ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار می‌دهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.

ما در اینجا DbSetهای هر دوی این DbContext‌ها را در ApplicationDbContext خود، خلاصه و ادغام می‌کنیم.


انتقال نگاشت‌های AutoMapper بسته‌ی دریافتی به پروژه‌ی جدید DNT.IDP.Mappings

در پوشه‌ی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشت‌های AutoMapper، برای تبدیلات بین موجودیت‌های برنامه و IdentityServer4.Models انجام شده‌است. کل محتویات این پوشه را به یک پروژه‌ی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آن‌را نیز اصلاح می‌کنیم.


انتقال src\IdentityServer4.EntityFramework\Options به پروژه‌ی DNT.IDP.Models

در پوشه‌ی Options بسته‌ی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینت‌ها است. نیازی به آن نداریم؛ چون زمانیکه موجودیت‌ها و تنظیمات آن‌ها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکن‌ها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دوره‌ای توکن‌های قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options
{
    public class OperationalStoreOptions
    {
        public bool EnableTokenCleanup { get; set; } = false;
        public int TokenCleanupInterval { get; set; } = 3600;
        public int TokenCleanupBatchSize { get; set; } = 100;
    }
}
از این تنظیمات در سرویس TokenCleanup استفاده می‌شود. به همین جهت همین سه مورد را به پروژه‌ی DNT.IDP.Models منتقل کرده و سپس بجای اینکه این کلاس را مستقیما در سرویس TokenCleanup تزریق کنیم، آن‌را از طریق سیستم Configuration و فایل appsettings.json به این سرویس تزریق می‌کنیم؛ به کمک سرویس توکار IOptions خود ASP.NET Core:
public TokenCleanup(
  IServiceProvider serviceProvider, 
  ILogger<TokenCleanup> logger, 
  IOptions<OperationalStoreOptions> options)
ج) Options\TableConfiguration.cs
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفته‌است. نیازی به آن نداریم.


انتقال سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP.Services

بسته‌ی دریافتی، شامل دو پوشه‌ی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویس‌های آن‌را تشکیل می‌دهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویس‌ها را نیز مستقیما از این پوشه‌ها به پروژه‌ی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بسته‌ی دریافتی در سرویس‌های آن، تعریف شده‌اند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایه‌ی سرویس قرار می‌گیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.


انتقال متدهای الحاقی معرفی سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP

پس از انتقال قسمت‌های مختلف IdentityServer4.EntityFramework به لایه‌های مختلف برنامه‌ی جاری، اکنون نیاز است سرویس‌های آن‌را به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظه‌ای کلاس آغازین برنامه‌ی IDP می‌شوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام می‌دهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژه‌ی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاس‌های تنظیمات را حذف کردیم و نیازی به آن‌ها نداریم، آن‌ها را نیز به طور کامل از این فایل حذف می‌کنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آن‌را از فایل appsettings.json دریافت می‌کنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بسته‌ی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفته‌اند. ما در اینجا آن‌ها را نیز با تک Context برنامه‌ی خود تعویض می‌کنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شده‌است. آن‌را نیز به لایه‌ی سرویس‌ها منتقل می‌کنیم. همچنین در امضای سازنده‌ی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شده‌ی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده می‌کنید.


افزودن متدهای الحاقی جدید به فایل آغازین برنامه‌ی IDP

پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژه‌ی DNT.IDP، اکنون نوبت به استفاده‌ی از آن است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddConfigurationStore()
                .AddOperationalStore();
به این ترتیب متدهای الحاقی جدید AddConfigurationStore و AddOperationalStore جهت معرفی محل‌های ذخیره سازی اطلاعات کاربران، منابع و توکن‌های IdentityServer مورد استفاده قرار می‌گیرند.


اجرای Migrations در پروژه‌ی DNT.IDP.DataLayer

پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیت‌ها و روابط جدید بین آن‌ها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشه‌ی src\IDP\DNT.IDP.DataLayer را اجرا می‌کنیم تا کلاس‌های Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا می‌کنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.


انتقال اطلاعات فایل درون حافظه‌ای Config، به بانک اطلاعاتی برنامه

تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمی‌کند. چون جداول کلاینت‌ها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظه‌ای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کرده‌ایم:
    public interface IConfigSeedDataService
    {
        void EnsureSeedDataForContext(
            IEnumerable<IdentityServer4.Models.Client> clients,
            IEnumerable<IdentityServer4.Models.ApiResource> apiResources,
            IEnumerable<IdentityServer4.Models.IdentityResource> identityResources);
    }
این سرویس به کمک اطلاعات Mappings مخصوص AutoMapper این پروژه، IdentityServer4.Models تعریف شده‌ی در کلاس Config درون حافظه‌ای را به موجودیت‌های اصلی DNT.IDP.DomainClasses.IdentityServer4Entities تبدیل و سپس در بانک اطلاعاتی ذخیره می‌کند.
برای استفاده‌ی از آن، به کلاس آغازین برنامه‌ی IDP مراجعه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            initializeDb(app);
            seedDb(app);
    // ...
        }
        
        private static void seedDb(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>();
                configSeedDataService.EnsureSeedDataForContext(
                    Config.GetClients(),
                    Config.GetApiResources(),
                    Config.GetIdentityResources()
                    );
            }
        }
در اینجا توسط متد seedDb، متدهای درون حافظه‌ای کلاس Config به سرویس ConfigSeedDataService ارسال شده، توسط Mappings تعریف شده، به معادل‌های موجودیت‌های برنامه تبدیل و سپس در بانک اطلاعاتی ذخیره می‌شوند.


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

پس از اعمال این تغییرات، برنامه‌ها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شده‌اند ملاحظه می‌کنید:


همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شده‌ی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:


و یا قسمت ذخیره سازی خودکار توکن‌های تولیدی توسط آن نیز به درستی کار می‌کند:





کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب مشابه
  • #
    ‫۵ سال و ۱۱ ماه قبل، سه‌شنبه ۳ مهر ۱۳۹۷، ساعت ۱۶:۰۸
    یک نکته‌ی تکمیلی: طراحی رابط کاربری برای یک IDP
    در سایت Auth0 یک اکانت ایجاد کنید. در اینجا می‌توان نحوه‌ی طراحی یک رابط کاربری را برای مفاهیم مختلف یک IDP که در این سری بررسی شدند، مشاهده کنید:

  • #
    ‫۵ سال و ۱۱ ماه قبل، چهارشنبه ۴ مهر ۱۳۹۷، ساعت ۱۱:۴۹
    یک نکته‌ی تکمیلی
    Identity Server به همراه یک Admin UI هم هست (برای مدیریت جداولی که در این قسمت اضافه شدند). این مورد تجاری است و حدود 500 یورو قیمت دارد. بجای آن می‌توان از پروژه‌ی skoruba / IdentityServer4.Admin نیز استفاده کرد (با مجوز MIT):

    پ.ن.
    اگر نیاز به پشتیبانی در مورد این سیستم ثالث دارید، لطفا به صفحه‌ی issue tracker آن مراجعه کنید.
    • #
      ‫۲ سال و ۱۰ ماه قبل، چهارشنبه ۲۸ مهر ۱۴۰۰، ساعت ۱۸:۴۱
      میخوام یه پروژه MVC رو به تمپلیت Skoruba به عنوان یک کلاینت جدید اضافه کنم. تو تنظیمات Sturtup پروژه Mvc اینگونه عمل کردم:
              public void ConfigureServices(IServiceCollection services)
              {
                  services.AddControllersWithViews();
                  IdentityModelEventSource.ShowPII = true;
                  services.AddAuthentication(options =>
                      {
                          options.DefaultScheme = "Cookies";
                          options.DefaultChallengeScheme = "oidc";
             options.DefaultSignOutScheme = "oidc";
                      })
                      .AddCookie("Cookies", options =>
                      {
                          options.AccessDeniedPath = "/Authorization/AccessDenied";
                          // set session lifetime
                          options.ExpireTimeSpan = TimeSpan.FromHours(8);
                          // sliding or absolute
                          options.SlidingExpiration = false;
                          // host prefixed cookie name
                          options.Cookie.Name = "MVC";
                          // strict SameSite handling
                          options.Cookie.SameSite = SameSiteMode.Strict;
                      })
                      .AddOpenIdConnect("oidc", options =>
                      {
                          options.SignInScheme = "Cookies";
                          options.Authority = Configuration["IDPBaseAddress"]; 
                          options.ClientId = Configuration["ClientId"];
                          options.ClientSecret = Configuration["ClientSecret"];                      
                          options.ResponseType = "code id_token";
                          options.ResponseMode = "query";
      
                          options.RequireHttpsMetadata = false;
                          options.CallbackPath = new PathString("/Home/");
                          options.SignedOutCallbackPath = new PathString("/Home/");
      
                          options.MapInboundClaims = true;
      
                          options.Scope.Clear();
                          options.Scope.Add("openid");
                          options.Scope.Add("profile");
                          options.Scope.Add("roles");
                          options.Scope.Add("PS.WebApi.Read");
                          options.Scope.Add("offline_access");
                          
                          options.SaveTokens = true;
                          options.GetClaimsFromUserInfoEndpoint = true;
                          //options.UsePkce = true;
                          //options.ClaimActions.MapJsonKey(claimType: "role", jsonKey: "role"); // for having 2 or more roles
                          
                          options.TokenValidationParameters = new TokenValidationParameters
                          {
                              NameClaimType = JwtClaimTypes.GivenName,
                              RoleClaimType = JwtClaimTypes.Role
                          };
                      });
                  //ServicePointManager.Expect100Continue = true;
                  //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
                  //                                       | SecurityProtocolType.Tls11
                  //                                       | SecurityProtocolType.Tls12
                  //                                       | SecurityProtocolType.Ssl3;
              }
      
              public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
              {
                  if (env.IsDevelopment())
                  {
                      app.UseDeveloperExceptionPage();
                  }
                  else
                  {
                      app.UseExceptionHandler("/Home/Error");
                      app.UseHsts();
                  }
                  app.UseHttpsRedirection();
                  app.UseStaticFiles();
      
                  app.UseRouting();
      
                  app.UseAuthentication();
                  app.UseAuthorization();
      
                  app.UseEndpoints(endpoints =>
                  {
                      endpoints.MapControllerRoute(
                          name: "areas",
                          pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
                      );
                      //.RequireAuthorization();
      
                      endpoints.MapControllerRoute(
                          name: "default",
                          pattern: "{controller=Home}/{action=Index}/{id?}"
                      );
                      //.RequireAuthorization();
                  });
      
                  //[HttpPost]
                  //public IActionResult Logout()
                  //{
                  //    return SignOut("Cookies", "oidc");
                  //}
              }

      پروژه‌های بنده به ترتیب Endpoint هاشون اینگونه هست:
      Skoruba.IdentityServer4.Admin  = https://localhost:44303
      Skoruba.IdentityServer4.STS.Identity   = https://localhost:44310
      Skoruba.IdentityServer4.Admin.Api    = https://localhost:44356 
      Mvc_Client_Project = https://localhost:44332
      تنظیمات کانفیک پروژه MVC: (appsettings.json)
        "WebApiBaseAddress": "https://localhost:44356",
        "IDPBaseAddress": "https://localhost:44310",
        "ClientId": "Mvc_ClientId",
        "ClientSecret": "WebMvc"
      محتویات فایل Identityserverdata.json:

      {
          "IdentityServerData": {
              "IdentityResources": [
                  {
                      "Name": "roles",
                      "Enabled": true,
                      "DisplayName": "Roles",
                      "UserClaims": [
                          "role"
                      ]
                  },
                  {
                      "Name": "openid",
                      "Enabled": true,
                      "Required": true,
                      "DisplayName": "Your user identifier",
                      "UserClaims": [
                          "sub"
                      ]
                  },
                  {
                      "Name": "profile",
                      "Enabled": true,
                      "DisplayName": "User profile",
                      "Description": "Your user profile information (first name, last name, etc.)",
                      "Emphasize": true,
                      "UserClaims": [
                          "name",
                          "family_name",
                          "given_name",
                          "middle_name",
                          "nickname",
                          "preferred_username",
                          "profile",
                          "picture",
                          "website",
                          "gender",
                          "birthdate",
                          "zoneinfo",
                          "locale",
                          "updated_at"
                      ]
                  },
                  {
                      "Name": "email",
                      "Enabled": true,
                      "DisplayName": "Your email address",
                      "Emphasize": true,
                      "UserClaims": [
                          "email",
                          "email_verified"
                      ]
                  },
                  {
                      "Name": "address",
                      "Enabled": true,
                      "DisplayName": "Your address",
                      "Emphasize": true,
                      "UserClaims": [
                          "address"
                      ]
                  }
              ],
              "ApiScopes": [
                {
                  "Name": "Idp_Admin_ClientId_api",
                  "DisplayName": "Idp_Admin_ClientId_api",
                  "Required": true,
                  "UserClaims": [
                    "role",
                    "name"
                  ]
                },
                {
                  "Name": "WebApi.Read",
                  "DisplayName": "WebApi Read",
                  "Required": true,
                  "UserClaims": [
                    "role",
                    "WebApi.Read"
                  ]
                },
                {
                  "Name": "WebApi.Write",
                  "DisplayName": "WebApi Write",
                  "Required": true,
                  "UserClaims": [
                    "role",
                    "WebApi.Write"
                  ]
                }
              ],
              "ApiResources": [
                {
                  "Name": "Idp_Admin_ClientId_api",
                  "Scopes": [
                    "Idp_Admin_ClientId_api"
                  ]
                },
                {
                  "Name": "WebApi",
                  "Scopes": [
                    "WebApi.Read",
                    "WebApi.Write"
                  ]
                }
              ],
            "Clients": [
              {
                "ClientId": "Idp_Admin_ClientId",
                "ClientName": "Idp_Admin_ClientId",
                "ClientUri": "https://localhost:44303",
                "AllowedGrantTypes": [
                  "authorization_code"
                ],
                "RequirePkce": true,
                "ClientSecrets": [
                  {
                    "Value": "Idp_Admin_ClientSecret"
                  }
                ],
                "RedirectUris": [
                  "https://localhost:44303/signin-oidc"
                ],
                "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
                "PostLogoutRedirectUris": [
                  "https://localhost:44303/signout-callback-oidc"
                ],
                "AllowedCorsOrigins": [
                  "https://localhost:44303"
                ],
                "AllowedScopes": [
                  "openid",
                  "email",
                  "profile",
                  "roles"
                ]
              },
              {
                "ClientId": "Idp_Admin_ClientId_api_swaggerui",
                "ClientName": "Idp_Admin_ClientId_api_swaggerui",
                "AllowedGrantTypes": [
                  "authorization_code"
                ],
                "RequireClientSecret": false,
                "RequirePkce": true,
                "RedirectUris": [
                  "https://localhost:44302/swagger/oauth2-redirect.html"
                ],
                "AllowedScopes": [
                  "Idp_Admin_ClientId_api"
                ],
                "AllowedCorsOrigins": [
                  "https://localhost:44302"
                ]
              },
              //WebApi
              {
                "ClientId": "WebApi_ClientId",
                "ClientName": "WebApi_ClientId",
                "ClientUri": "https://localhost:44365",
                "AllowedGrantTypes": [
                  "authorization_code"
                ],
                "RequirePkce": true,
                "ClientSecrets": [
                  {
                    "Value": "WebApi"
                  }
                ],
                "RedirectUris": [
                  "https://localhost:44303/signin-oidc"
                ],
                "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
                "PostLogoutRedirectUris": [
                  "https://localhost:44303/signout-callback-oidc"
                ],
                "AllowedCorsOrigins": [
                  "https://localhost:44303",
                  "https://localhost:44310"
                ],
                "AllowedScopes": [
                  "openid",
                  "email",
                  "profile",
                  "roles"
                ]
              },
              //Mvc
              {
                "ClientId": "Mvc_ClientId",
                "ClientName": "Mvc_ClientId",
                "ClientUri": "https://localhost:44332",
                "AllowedGrantTypes": [
                  "hybrid"
                ],
                //"RequirePkce": true,
                "AllowPlainTextPkce": false,
                "ClientSecrets": [
                  {
                    "Value": "WebMvc"
                  }
                ],
      
                "RedirectUris": [
                  "https://localhost:44332/signin-oidc"
                ],
                "FrontChannelLogoutUri": "https://localhost:44332/signout-oidc",
                "PostLogoutRedirectUris": [
                  "https://localhost:44332/signout-callback-oidc"
                ],
                "AllowedCorsOrigins": [
                  "https://localhost:44332",
                  "https://localhost:44310"
                ],
                "AllowedScopes": [
                  "openid",
                  "email",
                  "profile",
                  "roles",
                  "address",
                  "PS.webApi"
                ],
                "AllowAccessTokensViaBrowser": true,
                "RequireConsent": false,
                "AllowOfflineAccess": true
                //"UpdateAccessTokenClaimsOnRefresh": true
              }
            ]
          }
      }

      کنترلر Home در پروژه MVC:
          public class HomeController : Controller
          {
              private readonly ILogger<HomeController> _logger;
      
              public HomeController(ILogger<HomeController> logger)
              {
                  _logger = logger;
              }
      
              public IActionResult Default()
              {
                  return View();
              }
              public IActionResult Index()
              {
                  return View();
              }
      
              [Authorize]
              public IActionResult Privacy()
              {
                  return View();
              }
      
              [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
              public IActionResult Error()
              {
                  return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
              }
          }

      اما در نهایت بعد از اجرا و مراجعه به آدرس https://localhost:44332/home/privacy که مزین به اتریبیوت
      [Authorize]  
      می‌باشد با خطای زیر مواجه می‌شوم:

      لازم به توضیح هست که پروپرتی RequireHttpsMetadata = false می‌باشد.

    • #
      ‫۲ سال و ۱۰ ماه قبل، یکشنبه ۲ آبان ۱۴۰۰، ساعت ۱۱:۴۵
      در پروژه Skoruba  صفحه ثبت نام برای همه در دسترس است و هر برنامه کلاینتی  میتواند با واردکردن آدرس صفحه ثبت نام به این صفحه دسترسی داشته باشد.
      آیا نباید فقط کلاینتی که در قسمت مدیریتی  به صورت زیر تعریف میشود بتواند به این صفحه درخواست ارسال کند؟
      "Clients": [
                  {
                      "ClientId": "skoruba_identity_admin",
                      "ClientName": "skoruba_identity_admin",
                      "ClientUri": "https://localhost:44303",
                      "AllowedGrantTypes": [
                          "authorization_code"
                      ],
                      "RequirePkce": true,
                      "ClientSecrets": [
                          {
                              "Value": "skoruba_admin_client_secret"
                          }
                      ],
                      "RedirectUris": [
                          "https://localhost:44303/signin-oidc"
                      ],
                      "FrontChannelLogoutUri": "https://localhost:44303/signout-oidc",
                      "PostLogoutRedirectUris": [
                          "https://localhost:44303/signout-callback-oidc"
                      ],
                      "AllowedCorsOrigins": [
                          "https://localhost:44303"
                      ],
                      "AllowedScopes": [
                          "openid",
                          "email",
                          "profile",
                          "roles"
                      ]
                  },
                  {
                      "ClientId": "skoruba_identity_admin_api_swaggerui",
                      "ClientName": "skoruba_identity_admin_api_swaggerui",
                      "AllowedGrantTypes": [
                          "authorization_code"
                      ],
                      "RequireClientSecret": false,
                      "RequirePkce": true,
                      "RedirectUris": [
                          "https://localhost:44302/swagger/oauth2-redirect.html"
                      ],
                      "AllowedScopes": [
                          "skoruba_identity_admin_api"
                      ],
                      "AllowedCorsOrigins": [
                          "https://localhost:44302"
                      ]
                  }
              ]

      و هر کلاینت دیگری نباید مجاز به استفاده از این اپلیکیشن باشد

    • #
      ‫۲ سال و ۴ ماه قبل، شنبه ۱۰ اردیبهشت ۱۴۰۱، ساعت ۱۸:۲۴
      با سلام. آیا پروژه skoruba  خودش یک پروژه کامل sso هست؟ یه کانتکسی هست به اسم AdminIdentityDbContext . آیا باید DbContextIdentity خودمون رو جاش بزاریم؟ ممکنه یه توضیح مختصر در مورد concept این پروژه و تنظیمات مربوط رو بفرمائید؟ متشکرم
      • #
        ‫۲ سال و ۴ ماه قبل، شنبه ۱۰ اردیبهشت ۱۴۰۱، ساعت ۱۸:۳۴
        - یک authorization server هست؛ یک UI هست برای Identity server.  
        -  authorization server یک لایه نیست. قرار نیست جزئی از برنامه‌ی شما باشد. یک برنامه‌ی کاملا «مستقل» هست. هدف اصلی آن هم همین مستقل بودن و نقش تامین هویت مرکزی را بازی کردن هست و گرنه اگر قرار باشد این‌ها با هم یکی شوند، شاید بهتر باشد از ASP.NET Core Identity استفاده کنید. 
        - این روش برای شرکتی طراحی شده که یک برنامه‌ی حسابداری دارد، یک برنامه‌ی مجزای منابع انسانی و مدیریت کارمندان، یک برنامه‌ی مجزای مالی و حقوق و دستمزد، یک برنامه‌ی مجزای حضور و غیاب، یک برنامه‌ی مجزای اعلانات شرکت و غیره. هر کدام از این برنامه‌ها هم یک دیتابیس مستقل دارند و قرار نیست تعاریف کاربران و اطلاعات و نقش‌های آن‌ها در بانک‌های اطلاعاتی هر کدام از این برنامه‌ها تکرار شوند. اینجا است که «برنامه‌ی مستقل» authorization server مرکزی معنا پیدا می‌کند.
        • #
          ‫۲ سال و ۴ ماه قبل، شنبه ۱۰ اردیبهشت ۱۴۰۱، ساعت ۲۳:۳۲
          ممنونم. ببینید من اومده بودم یه Idp با ورژن جدیدش (duende) راه اندازی کردم البته  ترکیبش با asp.net Identity core. الان اگه بخوام یه ادمین آماده داشته باشم مثل   skoruba   باید پروژ خودم رو با همه کاراهایی که انجام دادم بیخیال شم ؟ و بر اساس قالب skoruba برم جلو؟
  • #
    ‫۵ سال و ۶ ماه قبل، جمعه ۳ اسفند ۱۳۹۷، ساعت ۱۹:۰۱
    سلام ؛ در ER مربوط به موجودیت‌های‌های پروژه هیچ ارتباطی بین API Resource ‌ها با مثلا User ‌ها وجود ندارد و اگر ما بخواهیم کاربران مرتبط با یک API را مشخص کنیم چگونه باید آن را مدیریت کرد. آیا همه‌ی کاربران امکان مشاهده و ارتباط با همه‌ی API‌ها دارند؟
    • #
      ‫۵ سال و ۶ ماه قبل، جمعه ۳ اسفند ۱۳۹۷، ساعت ۲۲:۲۶
      برای درک این سیستم از قسمت آخر آن شروع نکنید. در قسمت‌های قبل در مورد مفهوم یک سیستم تامین هویت مرکزی، مفهوم کلاینت‌ها و دسترسی دادن به آن‌ها در یک IDP (نه دسترسی دادن به کاربران)، امکان محدود کردن دسترسی کاربران به قسمت‌های مختلف برنامه‌ها و یا Web APIها توسط User Claims آن‌ها و ... مفصل بحث شده‌است.
      برای مثال زمانیکه برنامه‌ای (کلاینتی) به IDP گوگل معرفی می‌شود، آیا تمام کاربران گوگل به آن دسترسی پیدا می‌کنند؟ بله. تمام آن‌ها بدون استثناء.
      اما آیا تمام آن‌ها می‌توانند با قسمت‌های مختلف برنامه‌ی ما کار کنند؟ خیر. نحوه‌ی مدیریت دسترسی‌های این کاربران، در قسمت‌های قبلی بحث شده‌اند.
  • #
    ‫۵ سال قبل، پنجشنبه ۳۱ مرداد ۱۳۹۸، ساعت ۰۶:۰۵
    چطور میشه این پروژه رو در حالت دیباگ اجرا کرد؟ من این سه پروژه Idp، webApi و mvcClient رو جدا کردم ولی وقتی که ران می‌کنم میگه 127.0.0.1:5001 مشغول می‌باشد. می‌خوام کل سناریو رو trace کنم. میشه راهنمایی کنید؟
    • #
      ‫۵ سال قبل، پنجشنبه ۳۱ مرداد ۱۳۹۸، ساعت ۰۶:۱۶
      - برای اجرای همزمان چندین پروژه در ویژوال استودیو ، بر روی solution کلیک راست کرده و properties آن‌را انتخاب کنید. سپس در پنجره‌ی ظاهر شده، multiple startup project را انتخاب و در آخر پروژه‌های مورد نظر را انتخاب و وضعیت None آن‌ها را به start تغییر دهید.
      - شماره پورت‌های این برنامه‌ها در فایل‌های Properties\launchSettings.json درج شده‌اند. اگر قرار است این سه برنامه با هم اجرا شوند، نباید این شماره‌ها یکی باشند. در پروژه‌ی ارسال شده، این مسایل رعایت شده‌اند (هم برای حالت اجرای توسط IIS Express و هم برای حالت اجرای توسط dotnet run): ^ و ^ و ^
  • #
    ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۲ مهر ۱۳۹۸، ساعت ۱۶:۵۰
    سلام، من برنامه کوچک ازIdentityServer4 نوشتم که در Local کار می‌کند اما وقتی Publish می‌گیرم و روی Host ، IIS می‌کنیم در زمان Authentication و استفاده از API با خطای زیر مواجه می‌شم:
    IDX20804: Unable to retrieve document from: 'https://localhost/.well-known/openid-configuration'. ---> 
    System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> 
    System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
    نکته اینجاست که از روی IIS وقتی برنامه را اجرا می‌کنیم مسیر زیر قابل دسترسی هست، اما از طریق اجرای برنامه با خطای بالا مواجه می‌شم.
    • #
      ‫۴ سال و ۱۱ ماه قبل، جمعه ۱۲ مهر ۱۳۹۸، ساعت ۱۷:۰۳
      - از برنامه‌ی Jexus Manager استفاده کنید تا این مراحل را با چند کلیک ساده در قسمت server certificates آن برای شما انجام دهد:
      - اما برای IIS و محیط کاری، نیاز به مجوز واقعی (و نه self signed certificate) دارید؛ مانند: «مراحل تنظیم Let's Encrypt در IIS» 
      - اگر شبکه داخلی ویندوزی است، نیاز خواهید داشت Root Certificate Authority را نصب و راه اندازی کنید.  
      - و یا کلا نیاز به کار اجباری با SSL را می‌توانید به صورت زیر لغو کنید:
      .AddOpenIdConnect("oidc", options =>
      {
         options.RequireHttpsMetadata = false;
      // ...
      
      .AddIdentityServerAuthentication(options =>
      {
         options.RequireHttpsMetadata = false;
      // ...
  • #
    ‫۳ سال و ۴ ماه قبل، شنبه ۴ اردیبهشت ۱۴۰۰، ساعت ۱۳:۰۹
    با سلام؛ من  از ids4 برای سیستم احراز هویت سازمان استفاده میکنم حالا چالشی که برای من پیش آمده است به این شکله که اگر برنامهA بر روی مرورگر کروم توسط یک یوزر لاگین شد، و همان یوزر اگر توسط مرورگر دیگر اقدام به لاگین شدن به سیستم را داشت ،خودبخود از مرورگر کروم لاگ اوت شود.
  • #
    ‫۳ سال قبل، دوشنبه ۱۸ مرداد ۱۴۰۰، ساعت ۰۰:۵۲
    سلام. با ارتقای IdentityServer4 به نسخه 4.1، متد EnsureSeedDataForContext دچار مشکل میشه. یعنی نیاز به IdentityServer4.Storage نسخه 2.5 هست. لازم به ذکر است که نسخه core پروژه رو به 5 ارتقا دادم. در این حالت برای پیاده سازی این متد باید چه کرد؟
  • #
    ‫۱ سال و ۴ ماه قبل، سه‌شنبه ۲۲ فروردین ۱۴۰۲، ساعت ۱۶:۱۴
    نسخه دات نت پروژه ای ک در آن از identity server  استفاده میشود به 7 ارتقا داده شده است , اما به علت آپدیت پکیج auto mapper به 12.0.1 , خطای The type initializer for 'IdentityServer4.EntityFramework.Mappers.IdentityResourceMappers' threw an exception. در زمان اتصال کلاینتها به آیدنتیتی سرور مشاهده می‌شود.
    ممنون میشم راهنمایی بفرمایید