عنوان:

‫بررسی پشتیبانی از Passkey (WebAuthn/FIDO2) در ASP.NET Core Identity در NET 10.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۰۶ ۰۸:۲۰
آدرس: www.dntips.ir
چکیده: رمزهای عبور همواره یکی از ضعیف‌ترین حلقه‌های زنجیره امنیت در برنامه‌های وب بوده‌اند. انتخاب رمزهای ضعیف، استفاده مجدد از آن‌ها، آسیب‌پذیری در برابر حملات فیشینگ (Phishing) و سرقت اطلاعات، هزینه‌های سنگینی را به کاربران و سازمان‌ها تحمیل می‌کند. در پاسخ به این چالش، NET 10. پشتیبانی بومی و درون‌برنامه‌ای (Built-in) از Passkey را بر پایه استانداردهای WebAuthn و FIDO2 به ASP.NET Core Identity اضافه کرده‌است. این قابلیت به توسعه‌دهندگان اکوسیستم دات‌نت اجازه می‌دهد تا امکان احراز هویت بدون رمز عبور (Passwordless) را با استفاده از حسگرهای بیومتریک (مانند Touch ID و Windows Hello)، نرم‌افزارهای مدیریت رمز عبور (مانند 1Password، Bitwarden و Microsoft Authenticator) و کلیدهای سخت‌افزاری امنیتی (مانند YubiKey) پیاده‌سازی کنند. این مقاله به بررسی دقیق معماری فنی، فرآیندهای ثبت و ورود، نحوه پیکربندی کد #C، مدیریت چالش‌های امنیتی و عملیاتی، و استراتژی‌های پیاده‌سازی در محیط‌های عملیاتی می‌پردازد.

مقدمه
هر فرآیند «بازیابی رمز عبور» که در برنامه‌ها پیاده‌سازی می‌شود، به‌نوعی پذیرش شکست در مدل‌های احراز هویت سنتی است. در این مدل، کاربر موظف است رشته‌ای از حروف و اعداد را حفظ کند و سرور نیز باید هشِ آن را ذخیره نماید؛ امری که دیتابیس برنامه‌ها را به هدفی جذاب برای مهاجمان تبدیل می‌کند.
قابلیت Passkey، مدل احراز هویت را از «اثبات آگاهی از یک راز مشترک (Shared Secret)» به «اثبات مالکیت یک کلید خصوصی (Private Key)» تغییر می‌دهد. در این ساختار:
  • کلید خصوصی: هرگز از دستگاه کاربر یا سخت‌افزار امنیتی خارج نمی‌شود.
  • کلید عمومی (Public Key): روی سرور و در دیتابیس ASP.NET Core Identity ذخیره می‌گردد.
در NET 10.، کلاس‌های SignInManager و UserManager به متدهای جدیدی مجهز شده‌اند که تولید گزینه‌ها (Options)، اعتبارسنجی ثبت (Attestation) و ورود (Assertion) را به صورت مستقیم و بدون نیاز به کتابخانه‌های شخص ثالث (Third-party) انجام می‌دهند.

مفاهیم پایه و اصول کارکرد WebAuthn
احراز هویت مبتنی بر Passkey شامل دو چرخه مستقل رمزنگاری است:

۱. ثبت‌نام یا گواهی (Attestation)
در این مرحله، مرورگر از احرازکننده (Authenticator) دستگاه کاربر می‌خواهد که یک زوج‌کلید جدید مبتنی بر رمزنگاری کلید عمومی بسازد.
  • سرور یک چالش تصادفی (Challenge) تولید کرده و به مرورگر می‌فرستد.
  • دستگاه کاربر، زوج‌کلید را تولید کرده، چالش را با کلید خصوصی امضا می‌کند و کلید عمومی را همراه با داده‌های امضاشده به سرور برمی‌گرداند.
  • سرور کلید عمومی و شناسه اعتبار (Credential ID) را به حساب کاربر متصل و ذخیره می‌کند.

۲. ورود یا ادعا (Assertion)
در این مرحله، کاربر هویت خود را اثبات می‌کند.
  • سرور یک چالش یک‌بارمصرف جدید تولید می‌کند.
  • دستگاه کاربر با تأیید بیومتریک یا PIN، چالش را توسط کلید خصوصی ذخیره‌شده امضا می‌کند.
  • سرور امضا را با کلید عمومی ذخیره‌شده اعتبارسنجی کرده و در صورت صحت، نشست (Session) کاربر را فعال می‌کند.
[ دستگاه / احرازکننده کاربر ]                             [ سرور ASP.NET Core 10 ]
   (حاوی کلید خصوصی)                                      (ذخیره‌کننده کلید عمومی)
          |                                                       |
          |----------- ۱. درخواست چالش ورود (Request Options) --->|
          |<---------- ۲. ارسال چالش تصادفی (Challenge) ----------|
          |                                                       |
 (تأیید بیومتریک / PIN و)                                        |
 (امضای چالش با کلید خصوصی)                                      |
          |                                                       |
          |----------- ۳. ارسال چالش امضاشده (Credential JSON) -->|
          |                                                       |
                                                             (اعتبارسنجی امضا با)
                                                            (کلید عمومی در دیتابیس)

گستره ابزارها و تامین‌کنندگان Passkey (Passkey Providers)
یکی از تصورات اشتباه این است که Passkey تنها محدود به اثر انگشت یا تشخیص چهره لپ‌تاپ است. در واقع، تمام ابزارها و برنامه‌هایی که از استاندارد WebAuthn/FIDO2 پشتیبانی می‌کنند قابل استفاده هستند:
تامین‌کنندگان ابری (Synced Passkeys):
  • Google Password Manager: همگام‌سازی بین دستگاه‌های اندروید و مرورگر Chrome.
  • Apple iCloud Keychain: همگام‌سازی بین آیفون، مک‌بوک و مرورگر Safari.
  • Microsoft Account: همگام‌سازی روی ویندوز و حساب‌های مایکروسافت.
مدیریت‌کننده‌های رمز عبور شخص ثالث (Third-Party Password Managers):
  • ابزارهایی مانند Bitwarden، 1Password و Dashlane که امکان استفاده از Passkey را به‌صورت چندپلتفرمی (Cross-Platform) فراهم می‌سازند.
برنامه Microsoft Authenticator:
  • تامین‌کننده مستقیم: مدیریت و ذخیره کلیدها روی سیستم‌عامل‌های iOS و Android.
  • ورود میان‌دستگاهی (Cross-Device Authentication): اسکن QR Code روی لپ‌تاپ توسط اپلیکیشن Authenticator گوشی و تأیید اثر انگشت روی گوشی برای ورود به وب‌سایت روی لپ‌تاپ (از طریق ارتباط امن کوتاه‌برد بلوتوث).
  • حساب‌های سازمانی (Entra ID): ذخیره‌سازی به‌صورت Device-bound (بدون همگام‌سازی ابری برای حفظ حداکثر امنیت سازمانی).
  • کلیدهای سخت‌افزاری امنیتی (Hardware Security Keys):
  • کلیدهای متصل‌شونده از طریق USB ،NFC یا بلوتوث مانند YubiKey، Google Titan و Feitian.

پیکربندی و پیاده‌سازی فنی در ASP.NET Core 10
۱. فعال‌سازی اسکیما و خدمات Identity
برای استفاده از این قابلیت، باید اسکیما دیتابیس به نسخه‌ی ۳ (Version3) ارتقا یابد تا جدول AspNetUserPasskeys ایجاد شود:
// در فایل Program.cs
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;
    options.User.RequireUniqueEmail = true;
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
    
    // فعال‌سازی اسکیمای نسخه ۳ برای پشتیبانی از جداول Passkey
    options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddPasskeys() // متد افزوده شده در .NET 10
.AddDefaultTokenProviders();

تنظیمات مربوط به دامنه‌ی اتکا (Relying Party) نیز به صورت زیر پیکربندی می‌شود:
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "contoso.com"; // Relying Party ID
    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.ChallengeSize = 64; // حجم چالش به بایت (پیش‌فرض: 32)
    options.UserVerificationRequirement = "required"; // الزام تأیید بیومتریک یا PIN
    options.ResidentKeyRequirement = "preferred";
});
نکته امنیتی در مورد ServerDomain: اگر مقدار ServerDomain تعیین نشود، Identity آن را از هدر Host درخواست استخراج می‌کند. در محیط عملیاتی (Production)، حتماً این مقدار را به‌صورت صریح تنظیم کنید تا از حملات Credential-Scoping جلوگیری شود.

۲. فرآیند ثبت‌نام (Registration Flow)
ثبت‌نام شامل دو بخش سمت سرور و سمت کلاینت است.
سمت سرور: تولید چالش ثبت‌نام
app.MapPost("/account/passkey-options", async (
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager) =>
{
    var user = await userManager.GetUserAsync(context.User);
    if (user is null)
        return Results.NotFound();

    var userId = await userManager.GetUserIdAsync(user);
    var userName = await userManager.GetUserNameAsync(user) ?? "User";

    // تولید گزینه‌های WebAuthn و ذخیره وضعیت چالش در کوکی موقت
    var optionsJson = await signInManager.MakePasskeyCreationOptionsAsync(new()
    {
        Id = userId,
        Name = userName,
        DisplayName = userName
    });

    return TypedResults.Content(optionsJson, contentType: "application/json");
}).RequireRateLimiting("authentication");

سمت کلاینت: فراخوانی مرورگر (JavaScript)
async function createPasskey(headers) {
  const response = await fetch('/account/passkey-options', {
    method: 'POST',
    headers,
    credentials: 'include' // ارسال کوکی چالش
  });
  
  const optionsJson = await response.json();
  const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);

  try {
    // نمایش پنجره بیومتریک/سخت‌افزار توسط مرورگر
    const credential = await navigator.credentials.create({ publicKey: options });
    return credential;
  } catch (err) {
    console.error('تأیید پاس‌کی ناموفق بود:', err.name, err.message);
    throw err;
  }
}

سمت سرور: اعتبارسنجی و ذخیره‌سازی
app.MapPost("/account/passkey-register", async (
    HttpContext context,
    UserManager<ApplicationUser> userManager,
    SignInManager<ApplicationUser> signInManager,
    [FromBody] string credentialJson) =>
{
    var user = await userManager.GetUserAsync(context.User);

    // اعتبارسنجی امضا، چالش و مبدأ (Origin) درخواست
    var attestationResult = await signInManager.PerformPasskeyAttestationAsync(credentialJson);
    if (!attestationResult.Succeeded)
        return Results.BadRequest(attestationResult.Failure.Message);

    // ذخیره کلید عمومی در جدول AspNetUserPasskeys
    var addResult = await userManager.AddOrUpdatePasskeyAsync(user!, attestationResult.Passkey);
    return addResult.Succeeded ? Results.Ok() : Results.BadRequest("خطا در ذخیره‌سازی Passkey");
});

۳. فرآیند ورود (Sign-In Flow)
سمت سرور: درخواست چالش ورود
app.MapPost("/account/passkey-request-options", async (
    SignInManager<ApplicationUser> signInManager,
    UserManager<ApplicationUser> userManager,
    string? username) =>
{
    var user = string.IsNullOrEmpty(username) 
        ? null 
        : await userManager.FindByNameAsync(username);

    // ارسال null امکان ورود بدون نام کاربری (Conditional UI) را فراهم می‌سازد
    var optionsJson = await signInManager.MakePasskeyRequestOptionsAsync(user);
    return TypedResults.Content(optionsJson, contentType: "application/json");
});

سمت سرور: اعتبارسنجی ورود و ایجاد نشست
app.MapPost("/account/passkey-signin", async (
    SignInManager<ApplicationUser> signInManager,
    [FromBody] string credentialJson) =>
{
    // اعتبارسنجی امضا با کلید عمومی، بررسی چالش و فعال‌سازی کوکی نشست
    var result = await signInManager.PasskeySignInAsync(credentialJson);
    return result.Succeeded ? Results.Ok() : Results.Unauthorized();
});

سمت کلاینت: استفاده از رابط کاربری مشروط (Conditional UI / Autofill)
با افزودن ویژگی autocomplete="username webauthn" به اینپوت نام کاربری، مرورگر هنگام فوکوس کاربر، لیست Passkeyهای موجود را به عنوان پیشنهاد خودکار (Autofill) نشان می‌دهد:
async function enableConditionalSignIn() {
  const response = await fetch('/account/passkey-request-options');
  const optionsJson = await response.json();
  const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);

  const credential = await navigator.credentials.get({
    publicKey: options,
    mediation: 'conditional' // فعال‌سازی حالت پیشنهاد خودکار
  });

  // ارسال credential به سرور برای متد PasskeySignInAsync
}

ملاحظات معماری، محدودیت‌ها و سخت‌سازی (Hardening)
اگرچه پیاده‌سازی بومی در .NET 10 بسیار ساده است، اما برای محیط‌های واقعی و عملیاتی باید الزامات زیر در نظر گرفته شوند:
۱. گستره محدود پیاده‌سازی بومی (Deliberate Scope)
مستندات مایکروسافت به صراحت بیان می‌کنند که این قابلیت برای سناریوهای رایج احراز هویت طراحی شده و یک کتابخانه عمومی WebAuthn نیست.
  • عدم اعتبارسنجی بیانیه Attestation به صورت پیش‌فرض: سرور هر نوع Attestation ارائه شده را بدون بررسی لیست سفید سخت‌افزاری می‌پذیرد. اگر در محیط‌های سازمانی نیاز دارید ثبت فقط به مدل‌های خاصی (مانند YubiKey 5 Series) محدود شود، باید از Hook سفارشی استفاده کنید یا به سراغ کتابخانه‌هایی نظیر fido2-net-lib بروید:
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.VerifyAttestationStatement = async context =>
    {
        // بررسی AAGUID سخت‌افزار در برابر لیست مجاز
        return true; // یا false در صورت عدم تایید
    };
});
  • احراز هویت عاملی (Primary Factor): در ASP.NET Core Identity، استفاده از Passkey به عنوان عامل اصلی ورود تلقی می‌شود و نه عامل دوم (2FA).

۲. استراتژی بازیابی حساب (Account Recovery)
اگر تنها روش ورود کاربر Passkey باشد و دستگاه خود را گم کند، در صورت عدم پیش‌بینی راهکار بازیابی، دسترسی او برای همیشه قطع خواهد شد.
  • راهکار: سیستم باید همواره یک کانال بازیابی تاییدشده مانند ایمیل تاییدشده، کدهای بازیابی یک‌بارمصرف (Recovery Codes)، یا برنامه‌های TOTP را به عنوان پشتیبان نگه دارد.

۳. ایزوله‌سازی معمارانه (Clean Architecture & Privacy Audit)
اطلاعات حساس احراز هویت نباید در لاگ‌های عمومی یا سیستم‌های مانیتورینگ افشا شوند. برای ثبت رویدادهای امنیتی (Audit Logging) بدون نقض حریم خصوصی، استفاده از هش دارای کلید (HMAC-SHA-256) برای آدرس‌های IP و عدم ثبت داده‌های خام JSON توصیه می‌شود:
public sealed class SecurityAuditService(
    IAuthenticationAuditRepository repository,
    IPrivacyHasher privacyHasher,
    IClock clock)
{
    public Task RecordAsync(SecurityAuditRequest request, CancellationToken cancellationToken = default)
    {
        var auditEvent = AuthenticationAuditEvent.Create(
            request.UserId,
            request.EventType,
            request.Succeeded,
            privacyHasher.Hash(request.IpAddress), // هش کردن IP جهت حفظ حریم خصوصی
            request.UserAgent,
            request.FailureCode,
            clock.UtcNow);

        return repository.AddAsync(auditEvent, cancellationToken);
    }
}

۴. امن‌سازی کوکی نشست و Data Protection
پس از انجام فرآیند ورود، کوکی نشست (Session Cookie) جایگزین اعتبارنامه‌ها می‌شود. تنظیمات زیر برای جلوگیری از حملات XSS و CSRF ضروری است:
builder.Services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = "__Host-PasskeyApp";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.Cookie.Path = "/";
    options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
    options.SlidingExpiration = true;
});

// اشتراک‌گذاری کلیدهای Data Protection در صورت وجود چند Instance (Replicas)
builder.Services.AddDataProtection()
    .SetApplicationName("PasskeyApp")
    .PersistKeysToFileSystem(new DirectoryInfo(@"/app/keys"));

مقایسه و جمع‌بندی استراتژی‌های احراز هویت

ویژگی / روشرمز عبور سنتی (Passwords)لینک جادویی (Magic Links)کلیدهای Passkey (WebAuthn)
مقاومت در برابر فیشینگبسیار ضعیفمتوسط (وابسته به ایمیل)بسیار بالا (مبتنی بر Origin)
تجربه کاربری (UX)ضعیف (نیازمند حفظ رمز)متوسط (نیازمند بررسی ایمیل)عالی (تأیید سریع بیومتریک)
امنیت دیتابیس سرورریسک بالای افشای هش‌هابدون ذخیره راز روی سرورذخیره‌سازی کلید عمومی (بی‌ارزش برای مهاجم)
پیچیدگی بازیابیسادهسادهنیازمند طراحی پیش‌فرض (Recovery Flow)

نتیجه‌گیری
پشتیبانی بومی از Passkey در ASP.NET Core Identity در .NET 10 گامی بلند به سمت حذف رمزهای عبور در برنامه‌های وب است. این ویژگی با ارائه متدهای ساده در SignInManager و UserManager و یکپارچگی با دیتابیس Identity، پیچیدگی‌های اولیه پیاده‌سازی پروتکل WebAuthn را از دوش توسعه‌دهندگان برمی‌دارد. با این حال، موفقیت در پیاده‌سازی این سیستم در محیط‌های عملیاتی مستلزم نگاهی جامع به کل چرخه مدیریت هویت است؛ مواردی نظیر اجباری بودن TLS/HTTPS، پیکربندی دقیق ServerDomain، پیاده‌سازی راهکارهای بازیابی حساب، ایزوله‌سازی لاگ‌های امنیتی و سخت‌سازی کوکی‌های نشست. استفاده از Passkey به عنوان مسیر اصلی ورود در کنار کانال‌های پشتیبان تاییدشده، بهترین تعادل را میان امنیت بالا و تجربه کاربری روان ایجاد می‌کند.