آموزش JSON Web Token (به اختصار JWT) و
پیاده سازی آن در برنامههای ASP.NET Core درسایت موجود است.
توکن JWT در حالت عادی به صورت Base64 رمزنگاری میشود که این نوع رمزنگاری به راحتی قابل رمزگشایی و خواندن است. سایتهای آنلاین زیادی برای رمزگشایی base64 موجود است؛ برای مثال کافی است توکن خود را در سایت
jwt.io کپی کنید و به راحتی محتوای بدنه توکن (Payload) را مشاهده کنید.
پس توکن JWT هیچ امنیتی در برابر خوانده شدن ندارد.
سادهترین راه حل، رمزنگاری دستی بدنه توکن میباشد که مثلا بر اساس کلیدی (که فقط سمت سرور نگهداری و مراقبت میشود) توکن را رمزنگاری کرده و به هنگام خواندن، آن را با همان کلید رمزگشایی کنیم. ولی این روش ضمن استاندارد نبودن، مشکلات خاص خودش را دارد و نیاز به سفارشی سازی زیادی، هم به هنگام تولید توکن و هم به هنگام خواندن توکن دارد.
اصولیترین راه، استفاده از رمزنگاری توکن به روش JSON Web Encryption (یا به اختصار JWE) است که در آن مشابه روش بالا ولی به صورت
استاندارد تعریف شده (و قابل فهم برای همه استفاده کنندگانی که با این استاندارد آشنایی دارند) است.
نکته :
- اگر از JWE استفاده نمیکنید، بهتر است اطلاعات حساسی مانند شماره تلفن کاربر (و شاید در مواردی حتی آیدی کاربر) را در بدنه توکن قرار ندهیم چرا که قابل خوانده شدن است (که در این صورت استفاده از Guid برای آیدی کاربر می تواند کمی مفید باشد چرا که حداقل آیدی بقیه کاربران قابل پیش بینی نمیباشد).
- توکن JWT هیچ امنیتی در برابر خوانده شدن ندارد؛ ولی به لطف امضای (signature) آن، در برابر تغییر محتوا، ایمن است؛ چرا که در صورت تغییر محتوای آن، دیگر مقدار hash محتوا با امضای آن همخوانی نداشته و عملا از اعتبار ساقط میگردد.
برای رمزنگاری JWT باید در هر دو مرحلهی "تولید توکن" و "اعتبارسنجی توکن" کلید و الگوریتم لازم برای رمزنگاری را مشخص کنیم. بدین منظور در جایی که توکن را تولید میکنیم، خواهیم داشت :
var secretKey = Encoding.UTF8.GetBytes("LongerThan-16Char-SecretKey"); // must be 16 character or longer
var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature);
var encryptionkey = Encoding.UTF8.GetBytes("16CharEncryptKey"); //must be 16 character
var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "UserName"), //user.UserName
new Claim(ClaimTypes.NameIdentifier, "123"), //user.Id
};
var descriptor = new SecurityTokenDescriptor
{
Issuer = _siteSetting.JwtSettings.Issuer,
Audience = _siteSetting.JwtSettings.Audience,
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes),
Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes),
SigningCredentials = signingCredentials,
EncryptingCredentials = encryptingCredentials,
Subject = new ClaimsIdentity(claims)
};
var tokenHandler = new JwtSecurityTokenHandler();
var securityToken = tokenHandler.CreateToken(descriptor);
string encryptedJwt = tokenHandler.WriteToken(securityToken);
کد بالا، مانند کد تولید یک توکن jwt معمولی است؛ تنها تفاوت آن، ایجاد و معرفی شیء encryptingCredentials است.
در خط چهارم، آرایه بایتی کلید لازم برای رمزنگاری (encryptionkey) گرفته شده و از روی آن encryptingCredentials ایجاد شدهاست. این کلید باید 16 کاراکتر باشد؛ در غیر اینصورت به هنگام تولید توکن، خطا دریافت خواهید کرد. رمزنگاری توکن، توسط این کلید و الگوریتم مشخص شده انجام خواهد شد.
سپس شیء تولید شده، به خاصیت EncryptingCredentials کلاس SecurityTokenDescriptor معرفی شدهاست و نهایتا متد tokenHandler.WriteToken توکن رمزنگاری شدهای را تولید میکند.
نتیجه کار این است که توکن تولید شده، بدون کلید مربوطه (که سمت سرور نگهداری میشود) قابل رمز گشایی نیست و اگر آن را در سایت jwt.io کپی کنید، جوابی دریافت نخواهید کرد.
در ادامه لازم است در مرحله اعتبار سنجی و رمزگشایی توکن در سمت سرور، کلید و الگوریتم لازم را به آن معرفی کنیم تا middleware مربوطه بتواند توکن دریافتی را رمزگشایی و سپس اعتبار سنجی کند. بدین منظور در متد ConfigureServices کلاس Startup.cs خواهیم داشت:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var secretkey = Encoding.UTF8.GetBytes("LongerThan-16Char-SecretKey");
var encryptionkey = Encoding.UTF8.GetBytes("16CharEncryptKey");
var validationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.Zero, // default: 5 min
RequireSignedTokens = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretkey),
RequireExpirationTime = true,
ValidateLifetime = true,
ValidateAudience = true, //default : false
ValidAudience = "MyWebsite",
ValidateIssuer = true, //default : false
ValidIssuer = "MyWebsite",
TokenDecryptionKey = new SymmetricSecurityKey(encryptionkey)
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = validationParameters;
});
کد بالا مانند کد فعال سازی احراز هویت توسط JWT معمولی در ASP.NET Core است؛ با این تفاوت که:
ابتدا آرایه بایتی همان کلید رمزنگاری (encryptionkey) که قبلا توکن را با آن رمزنگاری کرده بودیم، گرفته شده و سپس توسط مقداردهی خاصیت TokenDecryptionKey کلاس TokenValidationParameters، معرفی شده است.
ولی شاید این سؤال برایتان پیش آید که چرا الگوریتم رمزنگاری مشخص نشده است؟ پس سرور از کجا میفهمد که این توکن بر اساس چه الگوریتمی رمزنگاری شده است؟
دلیل آن این است که به هنگام تولید توکن، اسم الگوریتم مربوطه، داخل بخش header توکن نوشته میشود. اگر تصویر قبل را مشاهده کنید مقدار header توکن به شرح زیر است.
{
"alg": "A128KW",
"enc": "A128CBC-HS256",
"typ": "JWT"
}
پس سرور بر اساس این قسمت از توکن (header)، که هیچگاه رمزنگاری نمیشود، میفهمد که توسط چه الگوریتمی باید توکن را رمزگشایی کند که در اینجا A128CBC-HS256 (اختصار AES-128-CBC و HMAC-SHA256) است.
مثال کامل و قابل اجرای این مطلب را میتوانید از این ریپازیتوری دریافت کنید.