غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامههای مبتنی بر ASP.NET Core
نویسنده: وحید نصیری
تاریخ: ۱۴۰۲/۰۱/۰۷ ۱۴:۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;
namespace ASPNETCore2JwtAuthentication.Services;
/// <summary>
/// To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ISecurityService _securityService;
public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
{
_securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);
public string GetDeviceDetails(HttpContext context)
{
var ua = GetUserAgent(context);
if (ua is null)
{
return "unknown";
}
var client = Parser.GetDefault().Parse(ua);
var deviceInfo = client.Device.Family;
var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
//TODO: Add the user's IP address here, if it's a banking system.
return $"{deviceInfo}, {browserInfo}, {osInfo}";
}
public string GetDeviceDetailsHash(HttpContext context) =>
_securityService.GetSha256Hash(GetDeviceDetails(context));
public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);
public string GetCurrentUserTokenDeviceDetailsHash() =>
GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
{
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
return null;
}
return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
}
public bool HasCurrentUserTokenValidDeviceDetails() =>
HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);
public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
StringComparison.Ordinal);
private static string GetUserAgent(HttpContext context)
{
if (context is null)
{
return null;
}
return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
? userAgent.ToString()
: null;
}
} public string GetDeviceDetails(HttpContext context)
{
var ua = GetUserAgent(context);
if (ua is null)
{
return "unknown";
}
var client = Parser.GetDefault().Parse(ua);
var deviceInfo = client.Device.Family;
var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
//TODO: Add the user's IP address here, if it's a banking system.
return $"{deviceInfo}, {browserInfo}, {osInfo}";
} new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(), ClaimValueTypes.String, _configuration.Value.Issuer),
.AddJwtBearer(cfg =>
{
cfg.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
return tokenValidatorService.ValidateAsync(context);
},
};
}); .AddCookie(options =>
{
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
}); OnValidatePrincipal = context =>
<script src="https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@3.12.1/dist/fingerprint2.min.js"></script>
string fingerprint = "";
string fingerprintScript = @"<script>
const fpPromise = FingerprintJS.load();
fpPromise.then(fp => {
fp.get().then(result => {
const values = result.values;
const fingerprint = values.join('');
document.cookie = 'DeviceFingerprint=' + fingerprint + '; path=/';
});
});
</script>";
Response.Write(fingerprintScript); string fingerprint = Request.Cookies["DeviceFingerprint"]?.Value;
if (fingerprint == null)
{
// Device fingerprint cookie not found
} FormsAuthentication.SignOut();
Session.Abandon();
// clear authentication cookie HttpCookie cookie1 = new HttpCookie(FormsAuthentication.FormsCookieName, "");
cookie1.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie1);
// clear session cookie (not necessary for your current problem but i would recommend you do it anyway) SessionStateSection sessionStateSection = (SessionStateSection)WebConfigurationManager.GetSection("system.web/sessionState");
HttpCookie cookie2 = new HttpCookie(sessionStateSection.CookieName, "");
cookie2.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(cookie2);
FormsAuthentication.RedirectToLoginPage(); یک نکتهی تکمیلی: گوگل برای مواجه شدن با یک چنین مشکلاتی، در حال پیاده سازی «device bound sessions» است. همچنین نحوهی رمزنگاری اطلاعات کوکیهای کروم نیز در ویندوز بهروز خواهد شد و دیگر صرفا از API خود ویندوز استفاده نمیکنند.
یک نکتهی تکمیلی: به روز رسانی کتابخانهی UAParser
در این مطلب از کتابخانهی UAParser استفاده شد. این کتابخانه، چندسالی است که بهروز نشده؛ البته چون نیازی نبوده! در اصل، این کتابخانه از فایل yaml مخصوصی که به صورت جاسازی شده (embedded) در آن قرار دارد، برای شناسایی مرورگرها استفاده میکند (مفهوم استفاده از متد ()Parser.GetDefault همین مورد است). بنابراین یا باید خودتان این فایل yaml را دستی به روز کرده (کار مخزن کد فعال UAParser-Core، فقط همین یک مورد است) و سپس کتابخانه را مجددا کامپایل و استفاده کنید و یا میتوانید محتویات فایل yaml ذکر شده را دریافت و سپس با استفاده از متد Parser.FromYaml این کتابخانه، اطلاعات جدید دریافتی را پردازش و استفاده کنید؛ مانند UAParserService.