توقف پذیرش رمزهای عبور افشاشده: یکپارچهسازی 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، میتوان فرآیند ثبتنام و تغییر رمز عبور را توسعه داد و از ورود رمزهای لو رفته به پایگاه داده جلوگیری کرد.نکته فنی: هش SHA-1 از نظر رمزنگاری ضعیف است، اما در اینجا هدف حفظ مخفیانگی نیست، بلکه استفاده از آن به عنوان یک کلید جستجو (Lookup Key) در API طراحیشده توسط HIBP است.
Password: "password"
SHA-1: 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
└─┬─┘ └───────────────────┬───────────────────┘
Prefix (5) Suffix (35)
(ارسال به API) (بررسی در سمت کلاینت)IPasswordValidator داریم.namespace MyApp.Identity.Validation;
public class HaveIBeenPwnedOptions
{
/// <summary>
/// حداقل تعداد تکرار در افشاگریها برای رد شدن رمز عبور.
/// مقدار پیشفرض ۱ است.
/// </summary>
public int MinimumBreachCount { get; set; } = 1;
}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;
}
}
}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();| رمز عبور | بررسی بومی | بررسی HIBP | نتیجه |
abc | رد شد (طول کم) | اجرا نشد | رد شد: عدم احراز حداقل طول |
password | تایید | افشا شده (۹.۶ میلیون بار) | رد شد: رمز عبور افشاشده |
correcthorsebatterystaple | تایید | یافت نشد | تایید شد |
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);
}
}catch مقدار IdentityResult.Failed را برگردانید.IPasswordValidator اختصاصی در ASP.NET Core Identity، میتوانید بدون بهخطر انداختن حریم خصوصی کاربران، از ورود رمزهای عبور افشاشده جلوگیری کرده و ریسک حملات Credential Stuffing را کاهش دهید.