ایمنسازی برنامه با OpenID Connect و JWT
منبع: Coding Clean, Reliable, and Safe REST APIs with ASP.NET Core 8 — Anthony Giretti
اعتبار ترجمه: ترجمه با کمک هوش مصنوعی
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
فصل ۱۰ — ایمنسازی برنامه با OpenID Connect
تقریباً هر برنامه به سازوکاری برای شناسایی کاربری که قصد انجام Action دارد نیاز دارد. این فرایند Authentication (احراز هویت) است و نباید با Authorization (مجوزدهی) اشتباه گرفته شود. Authorization پس از احراز هویت تعیین میکند کاربر چه Permission یا Roleهایی دارد و مجاز به انجام چه عملیاتهایی است.
موضوعات فصل:
- مقدمهای بر OpenID Connect (OIDC)
- پیکربندی Authentication و Authorization در ASP.NET Core
- ارسال JSON Web Token (JWT) در Request و خواندن هویت کاربر
مقدمهای بر OpenID Connect
OpenID Connect (OIDC) یک استاندارد هویتی است که روی OAuth 2.0، پروتکل Authorization، قرار میگیرد. در OIDC مسئولیت Authentication به یک سرویس Third-party یا Identity Provider واگذار میشود. برنامهٔ محافظتشده لازم نیست منطق Login و اعتبارسنجی Credential را خودش پیادهسازی کند؛ Provider هویت کاربر را تأیید میکند و نتیجه را در قالب Token قابل اعتماد به Client و Resource ارائه میدهد.
وقتی این سرویس هویتی مستقل از یک برنامه باشد، میتوان آن را برای چند برنامه بهصورت مشترک استفاده کرد و Single Sign-On (SSO) ساخت. سه Actor اصلی عبارتاند از:
- Client، برای مثال Web App.
- Identity Provider.
- Protected Resource، مانند ASP.NET Core API.
شکل ۱۰-۱ — ارتباط سه Actor در OpenID Connect
Client ابتدا نزد Provider احراز هویت میشود. Provider یک JSON Web Token صادر میکند و Client آن را برای دسترسی به Protected Resource ارسال میکند. Resource Metadata و Signing Key Provider را دریافت میکند تا Issuer و Signature Token را اعتبارسنجی کند. پس از دریافت Metadata، اعتبارسنجی Token میتواند محلی انجام شود.
JWT شامل سه بخش Header، Payload و Signature است که بهشکل Base64Url Encoding شده و با نقطه از هم جدا میشوند. Signature برای تشخیص دستکاری Token استفاده میشود. استاندارد JWT در RFC 7519 تعریف شده است.
هدف فصل آموزش کامل OIDC نیست؛ فقط حداقل مفاهیم لازم برای استفاده از آن در ASP.NET Core است. Providerهای مشهور شامل Google، Facebook، Apple و Microsoft هستند.
شکل ۱۰-۲ — استفادهٔ Canva.com از Google، Facebook و Apple بهعنوان Provider
نمونههای کتاب از Microsoft Authentication Platform مبتنی بر Azure Active Directory استفاده میکنند، اما Configuration API از نظر اصل کار به Provider خاصی وابسته نیست. فرض ادامهٔ فصل این است که یک access_token معتبر در اختیار دارید و آن را بهعنوان Bearer Token استفاده میکنید.
پیکربندی Authentication و Authorization در ASP.NET Core
ابتدا Package زیر نصب میشود:
Microsoft.AspNetCore.Authentication.JwtBearer
Configuration شامل این اجزاست:
AddAuthentication Scheme پیشفرض Authentication و Challenge را روی JwtBearerDefaults.AuthenticationScheme قرار میدهد.
AddJwtBearer پارامترهای OIDC/JWT را تنظیم میکند؛ Authority آدرس Identity Provider و Audience شناسهٔ Application هدف Token است.
ValidateLifetime و ValidateIssuer برای اعتبارسنجی زمان و صادرکننده فعال میشوند.
ClockSkew اختلاف ساعت مجاز بین Issuer و API را در مثال روی پنج دقیقه قرار میدهد.
AddAuthorization زیرساخت Policy/Role Authorization را ثبت میکند.
در Pipeline نیز Middlewareهای UseAuthentication و UseAuthorization فعال میشوند.
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/136544d9-xxxx-xxxx-xxxx-10accb370679/v2.0";
options.Audience = "257b6c36-xxxx-xxxx-xxxx-6f2cd81cec43";
options.TokenValidationParameters.ValidateLifetime = true;
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5);
});
builder.Services.AddAuthorization();
...
var app = builder.Build();
...
app.UseCors(); // Before Authentication and Authorization middlewares
app.UseMiddleware<YourMiddleware>(); // Before Authentication and Authorization middlewares
app.UseAuthentication();
app.UseAuthorization();
...
app.Run();
Listing 10-1 — پیکربندی و فعالسازی Authentication/Authorization مبتنی بر JWT
الزام Authentication روی Endpoint
با RequireAuthorization میتوان یک Minimal Endpoint را فقط برای کاربر احراز هویتشده در دسترس قرار داد.
app.MapGet("/authenticated", () =>
{
return Results.Ok("Authenticated !");
}).RequireAuthorization();
Listing 10-2 — GET /authenticated با RequireAuthorization
این نمونه فقط Authentication را الزام میکند. برای سطح دسترسی دقیقتر، Role یا Policy استفاده میشود.
Authorization بر اساس Role و Policy
Identity Provider میتواند Roleهای کاربر را در Token قرار دهد. برنامه بر اساس قوانین تجاری خودش Policy تعریف میکند. مثال زیر Policy با نام SurveyCreator را میسازد که وجود Role همنام را الزامی میکند و سپس آن را روی Endpoint اعمال میکند.
builder.Services
.AddAuthorization(options =>
options.AddPolicy("SurveyCreator",
policy => policy.RequireRole("SurveyCreator")
));
...
var app = builder.Build();
...
app.UseAuthentication();
app.UseAuthorization();
...
app.MapGet("/authorized", () =>
{
return Results.Ok("Authorized !");
}).RequireAuthorization("SurveyCreator");
...
app.Run();
Listing 10-3 — GET /authorized با Policy نوع SurveyCreator
اگر Request به Endpoint محافظتشده بدون JWT معتبر برسد، ASP.NET Core پاسخ 401 Unauthorized میدهد. اگر کاربر Authenticate شده باشد اما Requirement مربوط به Authorization، مثلاً Role لازم، را نداشته باشد، پاسخ 403 Forbidden است.
ساختار JWT و Debug آن
JWT را میتوان Decode کرد تا Header و Payload آن دیده شود. بخش Payload اطلاعاتی مانند Issuer، Expiration، شناسهٔ User و Roleها را در Claimها نگه میدارد. Decode کردن Token هنگام Debug خطاهایی مانند Expiration نامناسب، Audience اشتباه یا Roleهای نادرست مفید است؛ اما Decode کردن بهتنهایی به معنی معتبر بودن Signature نیست و API همچنان باید Validation کامل Token را انجام دهد.
شکل ۱۰-۳ — نمونهٔ JWT Decodeشده با Role نوع SurveyCreator
ارسال JWT در Request و دریافت هویت کاربر
در HTTP، JWT معمولاً در Header با نام Authorization و Scheme نوع Bearer ارسال میشود:
Authorization: Bearer {YourJWT}
جریان معمول OIDC این است که UI یا Client نزد Provider احراز هویت کند، JWT بگیرد و سپس آن را به API Back-end بفرستد. مثالهای این بخش روی Debug و تست Back-end تمرکز دارند و فرض میکنند Token قبلاً تولید شده است.
فعال کردن Bearer Token در Swagger
Swagger UI را میتوان طوری تنظیم کرد که JWT را از کاربر بگیرد و خودکار در Requestهای API قرار دهد.
builder.Services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header required"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme {
Reference = new OpenApiReference {
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
});
Listing 10-4 — پیکربندی SwaggerGen برای دریافت JWT
پس از این Configuration دکمهٔ Authorize در Swagger UI ظاهر میشود.
شکل ۱۰-۴ — دکمهٔ Authorize در Swagger
در Dialog مربوط، Token به همراه Scheme نوع Bearer وارد میشود.
شکل ۱۰-۵ — قرار دادن JWT Bearer در Swagger
اگر Token معتبر و دارای Role لازم باشد، Endpoint محافظتشده با موفقیت پاسخ میدهد.
شکل ۱۰-۶ — پاسخ موفق GET /authorized در Swagger
استفاده از JWT در Postman
در Postman میتوان Authorization Type را روی Bearer Token قرار داد و خود JWT را Paste کرد؛ Postman Scheme نوع Bearer را به Header اضافه میکند.
شکل ۱۰-۷ — پاسخ GET /authorized در Postman با JWT معتبر
دسترسی به Claims کاربر
در Minimal Endpoint میتوان ClaimsPrincipal را مستقیماً دریافت کرد و Claimهای هویت جاری را خواند. Claimها دادههایی هستند که Identity Provider دربارهٔ User در Token صادر کرده است؛ مانند Name و Role.
شکل ۱۰-۸ — دسترسی به هویت کاربر و Claims
برای خواناتر کردن مصرف Claims، کتاب یک Service به نام UserProfile میسازد که IUserProfile را پیادهسازی و Name و Roles را از HttpContext.User استخراج میکند.
using System.Security.Claims;
namespace AspNetCore8MinimalApis.Identity;
public class UserProfile : IUserProfile
{
private readonly IHttpContextAccessor _context;
public UserProfile(IHttpContextAccessor context)
{
_context = context;
}
public string Name => _context.HttpContext.User?.Claims
.FirstOrDefault(x => x.Type == "name")?.Value;
public IEnumerable<string> Roles => _context.HttpContext.User?.Claims
.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value);
}
Listing 10-5 — کلاس UserProfile
در یک Service سفارشی نمیتوان مثل پارامتر Minimal Endpoint فرض کرد ClaimsPrincipal مستقیم Inject میشود؛ برای این کار IHttpContextAccessor استفاده میشود. لازم است IUserProfile با Implementation خود و AddHttpContextAccessor در DI ثبت شوند.
شکل ۱۰-۹ — خواندن هویت کاربر از طریق IUserProfile
جمعبندی فصل
این فصل تفاوت Authentication و Authorization، نقش OIDC و JWT، پیکربندی JwtBearer در ASP.NET Core، Policyهای Role-based، وضعیتهای ۴۰۱/۴۰۳، تست Token در Swagger و Postman و خواندن Claimهای User را بررسی کرد. با پایان این فصل تقریباً همهٔ اجزای یک API با پیچیدگی متوسط پوشش داده شدهاند؛ موضوع نهایی کتاب، Testing API است.
تصاویر منبع مرتبط با این بخش