بررسی پشتیبانی از 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، مدیریت چالشهای امنیتی و عملیاتی، و استراتژیهای پیادهسازی در محیطهای عملیاتی میپردازد.
SignInManager و UserManager به متدهای جدیدی مجهز شدهاند که تولید گزینهها (Options)، اعتبارسنجی ثبت (Attestation) و ورود (Assertion) را به صورت مستقیم و بدون نیاز به کتابخانههای شخص ثالث (Third-party) انجام میدهند.[ دستگاه / احرازکننده کاربر ] [ سرور ASP.NET Core 10 ]
(حاوی کلید خصوصی) (ذخیرهکننده کلید عمومی)
| |
|----------- ۱. درخواست چالش ورود (Request Options) --->|
|<---------- ۲. ارسال چالش تصادفی (Challenge) ----------|
| |
(تأیید بیومتریک / PIN و) |
(امضای چالش با کلید خصوصی) |
| |
|----------- ۳. ارسال چالش امضاشده (Credential JSON) -->|
| |
(اعتبارسنجی امضا با)
(کلید عمومی در دیتابیس)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();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 جلوگیری شود.
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");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");
});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();
});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
}fido2-net-lib بروید:builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
options.VerifyAttestationStatement = async context =>
{
// بررسی AAGUID سختافزار در برابر لیست مجاز
return true; // یا false در صورت عدم تایید
};
});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);
}
}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) |
SignInManager و UserManager و یکپارچگی با دیتابیس Identity، پیچیدگیهای اولیه پیادهسازی پروتکل WebAuthn را از دوش توسعهدهندگان برمیدارد. با این حال، موفقیت در پیادهسازی این سیستم در محیطهای عملیاتی مستلزم نگاهی جامع به کل چرخه مدیریت هویت است؛ مواردی نظیر اجباری بودن TLS/HTTPS، پیکربندی دقیق ServerDomain، پیادهسازی راهکارهای بازیابی حساب، ایزولهسازی لاگهای امنیتی و سختسازی کوکیهای نشست. استفاده از Passkey به عنوان مسیر اصلی ورود در کنار کانالهای پشتیبان تاییدشده، بهترین تعادل را میان امنیت بالا و تجربه کاربری روان ایجاد میکند.