عنوان:

‫توقف پذیرش رمزهای عبور افشاشده: یکپارچه‌سازی HaveIBeenPwned با ASP.NET Core Identity


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۱۵ ۱۰:۳۵
آدرس: www.dntips.ir
چکیده: سیاست‌های سنتی پیچیدگی رمز عبور (شامل الزام به استفاده از حروف کوچک، بزرگ، اعداد و نمادها) اغلب در برابر حملات پرکردن اعتبارنامه‌ها (Credential Stuffing) شکست می‌خورند؛ چرا که این قوانین تنها فرمت ظاهری رمز عبور را بررسی می‌کنند، نه میزان امنیت و افشا نشدن واقعی آن را. استاندارد NIST SP 800-63B توصیه می‌کند که رمزهای عبور جدید به‌جای پیچیدگی ظاهری، در برابر فهرست‌های افشاشده بررسی شوند. این مقاله به آموزش یکپارچه‌سازی سرویس Have I Been Pwned (HIBP) با فریم‌ورک بومی ASP.NET Core Identity از طریق پیاده‌سازی یک IPasswordValidator اختصاصی می‌پردازد. با استفاده از الگوریتم k-Anonymity، رمز عبور کاربر بدون به‌خطر افتادن حریم خصوصی بررسی شده و رمزهای عبور افشاشده به صورت خودکار رد می‌شوند.

مقدمه
در معماری‌های نوظهور وب، دفاع در برابر حملات اتوماتیک از ارکان اصلی امنیت به شمار می‌رود. رمز عبوری مانند Tr0ub4dor&3 تمامی قواعد پیچیدگی سنتی را برآورده می‌سازد، اما اگر در یک نشت داده تاریخی افشا شده باشد، هکرها آن را در فهرست حملات Dictionary یا Credential Stuffing خود قرار داده‌اند. بررسی‌های سنتنی پیچیدگی توانایی شناسایی چنین تهدیدی را ندارند. فریم‌ورک ASP.NET Core Identity مکانیزم انعطاف‌پذیری برای اعتبارسنجی رمزهای عبور فراهم می‌کند. با پیاده‌سازی رابط IPasswordValidator، می‌توان فرآیند ثبت‌نام و تغییر رمز عبور را توسعه داد و از ورود رمزهای لو رفته به پایگاه داده جلوگیری کرد.

مکانیزم کاری API سرویس HaveIBeenPwned
سرویس HIBP بیش از ۱۰ میلیارد رمز عبور افشاشده را در خود جای داده است. جهت استعلام بدون افشای رمز عبور کاربر، از الگوریتم k-Anonymity در نقطه پایانی https://api.pwnedpasswords.com/range/{prefix} استفاده می‌شود:
  • محاسبه هش SHA-1: هش SHA-1 رمز عبور ورودی محاسبه می‌شود (مثلاً ۴۰ کاراکتر هگزادسیمال).
  • ارسال پیشوند (Prefix): تنها ۵ کاراکتر اول هش به سمت API ارسال می‌گردد.
  • دریافت پاسخ: سرور تمامی پسوندهای (Suffix) هم‌پوشان با آن ۵ کاراکتر را به همراه تعداد تکرار افشا (Breach Count) برمی‌گرداند.
  • بررسی محلی: برنامه پسوند هش کاربر را در لیست دریافتی جستجو می‌کند.

نکته فنی: هش SHA-1 از نظر رمزنگاری ضعیف است، اما در اینجا هدف حفظ مخفیانگی نیست، بلکه استفاده از آن به عنوان یک کلید جستجو (Lookup Key) در API طراحی‌شده توسط HIBP است.
Password: "password"
 SHA-1:    5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
           └─┬─┘ └───────────────────┬───────────────────┘
         Prefix (5)               Suffix (35)
         (ارسال به API)          (بررسی در سمت کلاینت)

پیاده‌سازی اعتبارسنج اختصاصی (Custom Password Validator)
برای ASP.NET Core Identity نیاز به پیاده‌سازی کلاس سفارشی از IPasswordValidator داریم.

۱. تعریف تنظیمات (Options)
namespace MyApp.Identity.Validation;

public class HaveIBeenPwnedOptions
{
    /// <summary>
    /// حداقل تعداد تکرار در افشاگری‌ها برای رد شدن رمز عبور.
    /// مقدار پیش‌فرض ۱ است.
    /// </summary>
    public int MinimumBreachCount { get; set; } = 1;
}

۲. پیاده‌سازی کلاس CustomPasswordValidator
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Identity;

namespace MyApp.Identity.Validation;

public class CustomPasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly HaveIBeenPwnedOptions _options;

    public CustomPasswordValidator(
        IHttpClientFactory httpClientFactory,
        HaveIBeenPwnedOptions? options = null)
    {
        _httpClientFactory = httpClientFactory;
        _options = options ?? new HaveIBeenPwnedOptions();
    }

    public async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string? password)
    {
        if (string.IsNullOrEmpty(password))
        {
            return IdentityResult.Success;
        }

        var passwordBytes = Encoding.UTF8.GetBytes(password);

#pragma warning disable CA5350 // SHA-1 به عنوان کلید جستجو استفاده می‌شود
        var hashBytes = SHA1.HashData(passwordBytes);
#pragma warning restore CA5350

        var hash = Convert.ToHexString(hashBytes);
        var prefix = hash[..5];
        var suffix = hash[5..];

        var httpClient = _httpClientFactory.CreateClient("hibp");

        try
        {
            using var response = await httpClient.GetAsync(
                $"https://api.pwnedpasswords.com/range/{prefix}");

            if (!response.IsSuccessStatusCode)
            {
                // سیاست Fail-Open: قطعی موقت سرویس خارجی نباید ثبت‌نام را مسدود کند
                return IdentityResult.Success;
            }

            var body = await response.Content.ReadAsStringAsync();

            foreach (var line in body.AsSpan().EnumerateLines())
            {
                var colonIndex = line.IndexOf(':');
                if (colonIndex < 0) continue;

                var responseSuffix = line[..colonIndex];
                if (!responseSuffix.Equals(suffix.AsSpan(), StringComparison.OrdinalIgnoreCase))
                    continue;

                if (int.TryParse(line[(colonIndex + 1)..], out var count) && count >= _options.MinimumBreachCount)
                {
                    return IdentityResult.Failed(new IdentityError
                    {
                        Code = "PasswordIsBreached",
                        Description = "این رمز عبور قبلاً در درزهای اطلاعاتی عمومی افشا شده است و امنیت کافی ندارد."
                    });
                }
            }

            return IdentityResult.Success;
        }
        catch (HttpRequestException)
        {
            // سیاست Fail-Open در صورت اختلال در شبکه
            return IdentityResult.Success;
        }
    }
}

پیکربندی و ثبت در تزریق وابستگی (Dependency Injection)
در فایل Program.cs باید هدر User-Agent برای درخواست‌های HTTP تنظیم شده و اعتبارسنج به مجموعه تزریق وابستگی اضافه گردد:
var builder = WebApplication.CreateBuilder(args);

// ۱. تنظیم Client شبکه برای HIBP (الزامی بودن User-Agent)
builder.Services.AddHttpClient("hibp", client =>
{
    client.DefaultRequestHeaders.UserAgent.ParseAdd("MyCustomApp-IdentitySecurity/1.0");
    client.Timeout = TimeSpan.FromSeconds(5); // جلوگیری از معطل شدن کاربر
});

// ۲. ثبت ASP.NET Core Identity
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
    // تنظیمات پایه رمز عبور
    options.Password.RequiredLength = 8;
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddPasswordValidator<CustomPasswordValidator<IdentityUser>>();

// ثبت صریح برای رفع نیازمندی‌های سناریوهای خاص تزریق وابستگی
builder.Services.AddScoped<IPasswordValidator<IdentityUser>, CustomPasswordValidator<IdentityUser>>();
builder.Services.AddScoped<CustomPasswordValidator<IdentityUser>>();

var app = builder.Build();

فرآیند اجرا و اولویت‌بندی قوانین (Execution Pipeline)
اعتبارسنج‌های رمز عبور در ASP.NET Core Identity به صورت ترتیبی اجرا می‌شوند:
  • اعتبارسنجی بومی (Built-in Rules): چک کردن طول رمز عبور و کاراکترها.
  • اعتبارسنج‌های سفارشی (Custom Validators): به ترتیب ثبت در Container.

اگر رمز عبور در مرحله اول رد شود (مثلاً طول آن کمتر از ۸ کاراکتر باشد)، اعتبارسنج HIBP فراخوانی نشده و درخواست شبکه‌ای بی‌مورد ارسال نمی‌گردد (Short-Circuiting).

رمز عبوربررسی بومیبررسی HIBPنتیجه
abcرد شد (طول کم)اجرا نشدرد شد: عدم احراز حداقل طول
passwordتاییدافشا شده (۹.۶ میلیون بار)رد شد: رمز عبور افشاشده
correcthorsebatterystapleتاییدیافت نشدتایید شد

آزمون‌نویسی (Unit Testing)
تست زیر بدون نیاز به Mock کردن، قابلیت اعتبارسنجی را در برابر API واقعی بررسی می‌کند:
using Microsoft.AspNetCore.Identity;
using Xunit;

namespace MyApp.Tests;

public class CustomPasswordValidatorTests
{
    private class SimpleHttpClientFactory : IHttpClientFactory
    {
        public HttpClient CreateClient(string name) => new HttpClient();
    }

    [Fact]
    public async Task ValidateAsync_KnownBreachedPassword_ReturnsFailure()
    {
        var validator = new CustomPasswordValidator<IdentityUser>(new SimpleHttpClientFactory());
        var user = new IdentityUser { UserName = "testuser" };

        var result = await validator.ValidateAsync(null!, user, "password");

        Assert.False(result.Succeeded);
        Assert.Contains(result.Errors, e => e.Code == "PasswordIsBreached");
    }

    [Fact]
    public async Task ValidateAsync_UniquePassword_ReturnsSuccess()
    {
        var validator = new CustomPasswordValidator<IdentityUser>(new SimpleHttpClientFactory());
        var user = new IdentityUser { UserName = "testuser" };
        var uniquePassword = $"Secure_{Guid.NewGuid():N}_{Guid.NewGuid():N}";

        var result = await validator.ValidateAsync(null!, user, uniquePassword);

        Assert.True(result.Succeeded);
    }
}

نکات تکمیلی برای محیط عملیاتی
  • سیاست Fail-Open در برابر Fail-Closed: پیاده‌سازی فوق به صورت Fail-Open است؛ یعنی اگر API در دسترس نباشد، ثبت‌نام کاربر مسدود نمی‌شود. اگر معماری سیستم نیاز به امنیت سخت‌گیرانه‌تری دارد، می‌توانید در قطعه کد catch مقدار IdentityResult.Failed را برگردانید.
  • استفاده از Caching (پنهان‌سازی): پیشوندهای پرکاربرد ۵ کاراکتری SHA-1 را می‌توان در حافظه یا Redis کش کرد تا تعداد درخواست‌های outgoing کاهش یابد.
  • کاربرد در IdentityServer / Duende: اگر از Duende به عنوان Identity Provider روی ASP.NET Core Identity استفاده می‌کنید، پیاده‌سازی فوق مستقیماً توسط مکانیزم‌های بومی Identity فراخوانی شده و تمامی لایه‌های احراز هویت را پوشش می‌دهد.

نتیجه‌گیری
جابجایی تمرکز از «قوانین پیچیدگی ظاهری» به «سنجش افشاشدگی واقعی» گامی اساسی در ارتقای امنیت برنامه‌های دات‌نتی است. با پیاده‌سازی الگوریتم k-Anonymity در قالب یک IPasswordValidator اختصاصی در ASP.NET Core Identity، می‌توانید بدون به‌خطر انداختن حریم خصوصی کاربران، از ورود رمزهای عبور افشاشده جلوگیری کرده و ریسک حملات Credential Stuffing را کاهش دهید.