بررسی فرمت کوکی‌های ASP.NET Identity
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

فرمت کوکی‌های ASP.NET Identity از پروژه‌ی سورس باز Katana دریافت شده‌است و تولید آن پس از لاگین کاربر، شامل مراحل زیر می‌باشد:
1- با استفاده از کلاس ApplicationUser، شیء ClaimsPrincipal را تولید می‌کند.
2- به این ClaimsPrincipal اطلاعاتی مانند ApplicationUser.Id و SecurityStamp اضافه می‌شوند.
3- در ادامه، ClaimsPrincipal به OWIN و کلاس CookieAuthenticationHandler آن ارسال می‌شود.
4- کار کلاس CookieAuthenticationHandler، تولید و تنظیم اطلاعاتی مانند تاریخ صدور کوکی، تاریخ انقضای آن، نوع کوکی، مانند ماندگار بودن یا امن بودن (HTTPS) و امثال آن است. حاصل این مراحله، تولید یک AuthenticationTicket است.
5- در آخر، AuthenticationTicket و  ClaimsPrincipal به کلاس SecureDataFormat، برای ابتدا، serialize شدن اشیاء، رمزنگاری و در نهایت تبدیل آن‌ها به فرمت base64، ارسال می‌شوند.

جزئیات تکمیلی مرحله‌ی آخر آن نیز به این ترتیب است:
AuthenticationTicket با استفاده از کلاس TicketSerializer سریالایز می‌شود. پس از آن یک memory stream تشکیل شده و اطلاعات ClaimsIdentity و AuthenticationTicket سریالایز شده به آن ارسال می‌شوند. این memory stream با استفاده از الگوریتم GZip فشرده شده و برای پردازش بیشتر بازگشت داده می‌شود. مرحله‌ی بعد، رمزنگاری اطلاعات فشرده سازی شده‌است. برای این منظور از کلاس DpapiDataProtector دات نت استفاده می‌کنند. پس از رمزنگاری، استریم نهایی با فرمت base64 برای درج در HTTP Response آماده خواهد شد.

سؤال: چرا کوکی‌‌های یک کاربر معین لاگین شده‌ی توسط ASP.NET Identity، در مرورگرهای مختلف متفاوت است؟
هرچند اطلاعاتی مانند ApplicationUser.Id و SecurityStamp برای یک کاربر، در مرورگرهای مختلف یکسان هستند، اما در مرحله‌ی چهارم، ذکر شد که AuthenticationTicket دارای اطلاعات بیشتری مانند زمان تولید کوکی نیز هست. بنابراین اطلاعات نهایی رمزنگاری شده‌ی در این حالت که در زمان‌های مختلفی تولید شده‌اند، یکسان نخواهند بود.

سؤال: در ساب دومین‌های مختلف دومین مشخصی، چندین برنامه‌ی مختلف نصب شده‌اند. چگونه می‌توان از یک سیستم لاگین ASP.NET Identity برای تمام آن‌ها استفاده کرد؟
برای این منظور نیاز هست خاصیت CookieDomain را به صورت صریح مقدار دهی کرد. برای اینکار فایل Startup.Auth.cs را گشوده و  CookieAuthenticationOptions را تنظیم کنید:
var cookieAuthenticationOptions = new CookieAuthenticationOptions
{
    AuthenticationType  = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath           = new PathString("/Account/Login"),
    CookieDomain        = ".mydomain.com"
};
البته کار به همینجا ختم نمی‌شود. پس از آن نیاز است به ازای تمام دومین‌های موجود، یک machine key مشخص تنظیم شود. از این جهت که در مرحله‌ی پنجم تولید کوکی، کلاس DpapiDataProtector دات نت، از machine key موجود، برای رمزنگاری اطلاعات استفاده می‌کند و اگر این machine key، به ازای برنامه‌های مختلف متفاوت باشد، کوکی تولید شده، قابل رمزگشایی و استفاده نخواهد بود.
برای اینکار به کنسول IIS مراجعه کرده و گزینه‌ی machine key آن‌را بیابید. در این قسمت بر روی generate keys کلیک کرده و اطلاعات تولیدی را باید به تمام web.config‌های موجود کپی کنید:
 <machineKey
  validationKey="DAD9E2B0F9..."
  decryptionKey="ADD1C39C02..."
  validation="SHA1"
  decryption="AES"
/>

سؤال: برنامه‌های مختلفی بر روی یک دومین نصب هستند، اما قصد نداریم از سیستم اعتبارسنجی یکپارچه‌ای برای تمام آن‌ها استفاده کنیم. اما اگر در یکی لاگین کنیم، بلافاصله لاگین در برنامه‌ی دوم منقضی می‌شود، چرا؟
شبیه به همین مساله با Forms Authentication هم وجود دارد. برای رفع آن باید نام کوکی‌های هر برنامه را منحصربفرد کنید و از نام پیش فرض کوکی‌ها استفاده نکنید تا بر روی یکدیگر بازنویسی نشوند. برای اینکار خاصیت CookieName شیء CookieAuthenticationOptions را جداگانه مقدار دهی کنید:
 CookieName = "my-very-own-cookie-name"

سؤال: لاگین انجام شده‌ی در برنامه‌ای که از ASP.NET Identity استفاده می‌کند، زود منقضی می‌شود؛ چرا؟
برای تنظیم صریح زمان انقضای کوکی ASP.NET Identity نیاز است خاصیت ExpireTimeSpan آن‌را مقدار دهی کنید:
 app.UseCookieAuthentication(new CookieAuthenticationOptions
{
     ExpireTimeSpan = TimeSpan.FromHours(24.0),
});

سؤال: کاربر سیستم ASP.NET Identity از سیستم خارج شده‌است (log off کرده) ولی هنوز می‌توان از کوکی پیشین او برای اعتبارسنجی مجدد استفاده کرد. چطور می‌توان این نقیصه‌ی امنیتی را برطرف کرد؟
مشکل از اینجا است:
   public ActionResult LogOff()
  {
      AuthenticationManager.SignOut();
      return RedirectToAction("Index", "Home");
  }
در مثال رسمی ASP.NET Identity یک چنین کدی برای خروج از سیستم ارائه شده‌است. نمونه‌ی امن‌تر آن به صورت زیر است:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> LogOff()
{
    var user = await UserManager.FindByNameAsync(User.Identity.Name);
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    await UserManager.UpdateSecurityStampAsync(user.Id);
    return RedirectToAction("Login", "Account");
}
در اینجا علاوه بر عدم استفاده‌ی از متد بدون پارامتر SignOut (با توجه به خاصیت AuthenticationType ذکر شده‌ی در CookieAuthenticationOptions)، کار به روز رسانی مجدد SecurityStamp کوکی نیز انجام شده‌است. با این تغییر، کوکی موجود بلا استفاده خواهد شد؛ چون دیگر قابل رمزگشایی نیست.
همچنین بهتر است مقدار validateInterval مربوط به SecurityStampValidator.OnValidateIdentity که به صورت پیش فرض 30 دقیقه است را به مقدار کمتری مانند 5 دقیقه تغییر دهید (تنظیمات OnValidateIdentity مربوط به CookieAuthenticationOptions فایل آغارین برنامه). کار این تنظیم، بررسی اعتبار کوکی، در بازه‌های زمانی مشخص شده‌است.
  • #
    ‫۸ سال و ۹ ماه قبل، جمعه ۲۰ آذر ۱۳۹۴، ساعت ۲۳:۴۰
    با سلام
    آقای نصیری در آخرین سوال گفتید که باید از UpdateSecurityStampAsync استفاده بشه. من تو برنامم ، بر اساس هر Application_AuthenticateRequest میام چک میکنم که اگر کاربر حذف یا قفل یا ... بود به صورت خودکار Signout بشه. با توجه به نکته امنیتی که که شما گفتید باید اینجا هم از  UpdateSecurityStampAsync  استفاده بشه ، من به این صورت استفاده کردم :


       public class MvcApplication : HttpApplication
        {
            public MvcApplication()
            {
                var wrapper = new EventHandlerTaskAsyncHelper(AuthenticateRequest);
                AddOnAuthenticateRequestAsync(wrapper.BeginEventHandler, wrapper.EndEventHandler);
            }
            private async Task AuthenticateRequest(object sender, EventArgs e)
            {
                if (Context.User == null)
                    return;
    
                //bla bla
    
                if (هرچیزی که مجاز نباشد)
                {
                    var authenticationManager = ProjectObjectFactory.Container.GetInstance<IAuthenticationManager>();
                    authenticationManager.SignOut
                    (
                        DefaultAuthenticationTypes.ExternalCookie,
                        DefaultAuthenticationTypes.ApplicationCookie
                    );
                    FormsAuthentication.SignOut();
                    await userService.UpdateSecurityStampAsync(userId);
                }
            }
        }

    در عمل چک کردم جواب داد ، البته مجبور بودم از یک EventHandlerTaskAsyncHelper استفاده کنم چون نمیشد تو خود متد Application_AuthenticateRequest متدهای Async تعریف کرد.

    سوال من اینه ، آیا اصلا نیاز هست در اینجا هم security stamp رو آپدیت کنیم ؟ یا اصلا این روشی که من رفتم از نظر امنیتی درست هست یا خیر.

    با تشکر
    • #
      ‫۸ سال و ۹ ماه قبل، شنبه ۲۱ آذر ۱۳۹۴، ساعت ۰۱:۰۹
      با نگارش بعدی ASP.NET، مباحث Global.asax ، HTTP Module و امثال آن به طور کامل حذف خواهند شد. به همین جهت ASP.NET Identity از OWIN استفاده می‌کند که پایه‌ی ASP.NET 5 است و در اینجا از OWIN Middleware ‌ها برای این نوع سفارشی سازی‌ها کمک می‌گیرند.
    • #
      ‫۸ سال و ۹ ماه قبل، شنبه ۲۱ آذر ۱۳۹۴، ساعت ۰۲:۱۴
      این مورد اضافی است ؛ به صورت توکار  از موردی که مطرح کردید ، پشتیابی میشود.
      فقط کافی است هنگام غیرفعال سازی یا حذف نرم کاربر ، SecurityStamp مربوط به کاربر را ویرایش کنید ؛ و اگر زمان این خروج اتوماتیک برای شما خیلی مهم است ، باید validateInterval  رو هم در تنظیمات با مقدار :
      TimeSpan.FromMinutes(0)
      تنظیم کنید.
      • #
        ‫۸ سال و ۹ ماه قبل، شنبه ۲۱ آذر ۱۳۹۴، ساعت ۰۷:۴۷
        ببینید ، فرض کنید من پروژه آقای نصیری که در اینجا هست رو اجرا می‌کنم ( آخرین نسخه که موارد بالا رو هم داره ) ، و با نام کاربری و رمز عبوری که در seed قرار دادن وارد میشم. خب تا بدین جای کار مشکلی وجود نداره ، حالا من به صورت فیزیکی و توسط sql server تنها یوزری که وجود داره و من باهاش لاگین کردم رو حذف می‌کنم. ولی وقتی دوباره صفحه رو رفرش می‌کنم و به صفحاتی که نیاز به مجوز دارن می‌رم ، اصلا اتفاقی رخ نمیده و انگار واقعا اون کاربر وجود داره در صورتی که وقتی روی لینک Hello admin@example.com!  کلیک می‌کنم ، خطای زیر رخ میده : 

        UserId not found.

        به عبارتی کاربر وجود خارجی نداره ولی مجوز رفتن به تمامی بخش هایی که مجوز نیاز دارن رو داره. برای این مشکل باید چیکار کرد ؟ الان من validateInterval رو در پروزه آقای نصیری هم 0 کردم ولی باز کاربری که حذف شده داره تو صفحاتی که نیاز به مجوز دارن حرکت میکنه
        • #
          ‫۸ سال و ۹ ماه قبل، شنبه ۲۱ آذر ۱۳۹۴، ساعت ۱۷:۴۷
          مشکل رو بلاخره متوجه شدم.

          اگر به کدهای قرار داده شده در startup نگاه کنید ، همچین کدی وجود داره :
                app.CreatePerOwinContext(
                         () => SmObjectFactory.Container.GetInstance<IApplicationUserManager>());
          اومدم این کد رو با کد زیر هم تغییر دادم ، ولی اتفاقی رخ نداد :

                app.CreatePerOwinContext(
                         () => SmObjectFactory.Container.GetInstance<ApplicationUserManager>());

          بعدش اومدم این کد روابتدای دستورات Startup قرار دادم و به درستی کار کرد برنامه.! الان حتی به صورت فیزیکی هم کاربر حذف بشه پس از اولین درخواست کاربر به صفحه لاگین هدایت میشه. در پروژه خودمم همین کارو کردم و مشکل حل شد.
  • #
    ‫۷ سال و ۱ ماه قبل، شنبه ۷ مرداد ۱۳۹۶، ساعت ۱۶:۲۴
    سؤال: چرا به روز رسانی اطلاعات کاربر، سبب logout او می‌شود؟
    در ASP.NET Identity‌، جدول کاربران دارای فیلد SecurityStamp است و با مقایسه‌ی مقدار آن با مقدار موجود در کوکی کاربر، مشخص می‌کند آیا اطلاعات کاربری در سمت سرور تغییر کرده‌است یا خیر؟ اگر بله، این کاربر مجبور به لاگین مجدد خواهد شد.
     OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, User>(
                validateInterval: TimeSpan.FromMinutes(0),
                regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    اینکه چه زمانی بررسی مجدد SecurityStamp موجود در کوکی کاربر صورت می‌گیرد، توسط پارامتر validateInterval مشخص می‌شود. در اینجا این پارامتر به صفر تنظیم شده‌است. یعنی اطلاعات کاربر در درخواست بعدی، مجددا تعیین اعتبار می‌شود.
    تغییر اطلاعات فیلد SecurityStamp یا با فراخوانی مستقیم userService.UpdateSecurityStampAsync انجام می‌شود یا متدهای ذیل هم به صورت توکار شامل این فراخوانی در پشت صحنه هستند:
    UserManager.CreateAsync
    UserManager.RemovePasswordAsync
    UserManager.UpdatePassword
    UserManager.RemoveLoginAsync
    UserManager.ChangePhoneNumberAsync/SetPhoneNumberAsync
    UserManager.SetTwoFactorEnabledAsync
    UserManager.SetEmailAsync
    یعنی هر نوع تغییری در اطلاعات کاربر، سبب logout او خواهد شد.