xSaherelmWorkspace
Technical Analysis Document
شماره مستند: XIDS-AUTH-2026-001
نسخه: ۱.۰
موضوع: رفع مشکل احراز هویت XApiKey
طبقه‌بندی: داخلی — فنی
TECHNICAL DIAGNOSIS & SOLUTION

ریشه‌یابی و رفع مشکل احراز هویت با XApiKey در xIds

تحلیل جامع کد، شناسایی گلوگاه احراز هویت، و ارائه راه‌حل گام‌به‌گام برای فعال‌سازی کامل Authentication مبتنی بر API Key در سرور IdentityServer4
خلاصه اجرایی:
پس از بررسی کامل کدهای پروژه xIds، مشخص شد که زیرساخت مدیریت Application و تولید XApiKey به‌درستی پیاده‌سازی شده است، اما زنجیره احراز هویت مبتنی بر API Key در سمت سرور IdentityServer4 ناقص مانده است. این سند ابتدا ریشه مشکل را با استناد به کدهای موجود تشریح می‌کند، سپس راه‌حل کامل گام‌به‌گام را ارائه می‌دهد.
۱

وضعیت فعلی سیستم

Current State Analysis

در پروژه xIds، اجزای زیر برای مدیریت Application و API Key پیاده‌سازی شده‌اند:

۱.۱ — اجزای پیاده‌سازی‌شده

جزء مسیر فایل وضعیت
مدل‌های Application و ApiKey xIdentityModels/Models/XApplication.cs ✓ موجود
XApplicationProvider (مدیریت API Key) xIds/Providers/XApplicationProvider.cs ✓ موجود
XApiKeyHelper (تولید و هش) xIds/Helpers/XApiKeyHelper.cs ✓ موجود
ApplicationsController (Endpointها) xIds/Controllers/ApplicationsController.cs ✓ موجود
XApiKeyAuthenticationHandler xIds/Providers/XApiKeyAuthenticationHandler.cs ⚠ موجود اما ناقص
XApiKeyConfiguration xIds/Configurations/XApiKeyConfiguration.cs ✓ موجود
ثبت Scheme در Startup xIds/Startup.cs ✓ موجود

۱.۲ — معماری جاری احراز هویت در Startup.cs

// xIds/Startup.cs services.AddXIdentityServerAuthentication( configuration: Configuration, addApiKeyAuthentication: true // ← این خط فعال است );

و در XDIHelperExtension.AddXIdentityServerAuthentication:

var authBuilder = services.AddAuthentication(); authBuilder.AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; }) .AddLocalApi(); if (addApiKeyAuthentication) { authBuilder.AddScheme<AuthenticationSchemeOptions, XApiKeyAuthenticationHandler>( XAuthenticationScheme.XApiKey.GetStringValue(), // "XApiKey" options => {} ); }
نتیجه: به نظر می‌رسد همه چیز در Startup به‌درستی ثبت شده است. اما در عمل، درخواست‌هایی که Header X-Api-Key دارند، احراز هویت نمی‌شوند. علت چیست؟
۲

ریشه‌یابی مشکل

Root Cause Analysis

پس از بررسی دقیق کدها، سه ریشه اصلی برای عدم کارکرد احراز هویت با XApiKey شناسایی شد:

🔴 مشکل ۱: ForwardDefaultSelector به XApiKey توجه نمی‌کند

در AddJwtBearer، تنظیم ForwardDefault روی XAuthentication.IDENTITY_SERVER_LOCAL_API قرار داده شده است. این یعنی ASP.NET Core همیشه از Scheme پیش‌فرض (IdentityServerAccessToken) استفاده می‌کند و هیچ‌گاه به Scheme با نام XApiKey نمی‌رسد.

// xIds/DI/XDIHelperExtension.cs — AddXIdentityServerAuthentication authBuilder.AddJwtBearer(options => { options.SaveToken = true; options.RequireHttpsMetadata = false; options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; // ← مشکل اصلی اینجاست })
🔴 مشکل ۲: ترتیب ثبت Schemeها اشتباه است

در ASP.NET Core، اولین Scheme ثبت‌شده، Scheme پیش‌فرض است. در کد فعلی، AddJwtBearer قبل از AddScheme<...XApiKeyAuthenticationHandler> ثبت می‌شود و این باعث می‌شود حتی اگر Header X-Api-Key ارسال شود، Handler مربوطه هرگز فراخوانی نگردد.

🔴 مشکل ۳: XApiKeyAuthenticationHandler به Claimهای Policyهای موجود پاسخ نمی‌دهد

Policyهای موجود در XAuthorizationHelper (مانند ApiKeyAccess و ApiKeyReadAccess) انتظار Claim از نوع JwtClaimTypes.Scope با مقادیر read، write، admin یا manage دارند. اما XApiKeyAuthenticationHandler فعلی در کد، یک Scope ثابت به نام "apikey" اضافه می‌کند که با هیچ Policy موجودی مطابقت ندارد.

// xIds/Providers/XApiKeyAuthenticationHandler.cs — کد فعلی var claims = new[] { new Claim(ClaimTypes.Name, validationResult.OwnerId), new Claim("application_id", validationResult.ApplicationId.ToString()), new Claim("auth_type", "apikey"), new Claim(JwtClaimTypes.Scope, "apikey"), // ← هیچ Policyای این Scope را نمی‌شناسد }; // سپس scopes از validationResult اضافه می‌شود، اما با مقادیر "read", "write"... // که با Policyهای XAuthorizationHelper همخوانی ندارند

۲.۱ — درخت عیب‌یابی

✗
درخواست با Header: X-Api-Key: xapp_xxxxx

Header به درستی در Request وجود دارد.

✗
ASP.NET Core می‌خواهد Scheme پیش‌فرض را انتخاب کند

به دلیل ForwardDefault = IDENTITY_SERVER_LOCAL_API، همیشه Scheme پیش‌فرض انتخاب می‌شود.

✗
XApiKeyAuthenticationHandler فراخوانی نمی‌شود

چون هیچ‌گاه انتخاب نمی‌شود، متد HandleAuthenticateAsync اجرا نمی‌گردد.

✗
درخواست با خطای 401 Unauthorized رد می‌شود

پایان مسیر — احراز هویت شکست خورده است.

⚠️ خلاصه ریشه مشکل: در معماری فعلی، ASP.NET Core با ForwardDefault به Scheme پیش‌فرض هدایت می‌شود و هرگز به Scheme XApiKey نمی‌رسد. علاوه بر این، حتی اگر Handler فراخوانی شود، Claimهای تولیدشده با Policyهای موجود همخوانی ندارند و در نتیجه Authorization نیز شکست می‌خورد.
۳

نمودار جریان فعلی در مقابل جریان مطلوب

Current vs. Desired Auth Flow

۳.۱ — جریان فعلی (معیوب)

۱. Client Request

ارسال درخواست با Header X-Api-Key

▼
۲. Default Scheme Selection

ASP.NET Core به دلیل ForwardDefault، Scheme پیش‌فرض را انتخاب می‌کند

▼
۳. JwtBearer Handler اجرا می‌شود

به‌دنبال Bearer Token می‌گردد، پیدا نمی‌کند، شکست می‌خورد

▼
❌ نتیجه: 401 Unauthorized

XApiKeyAuthenticationHandler هرگز اجرا نمی‌شود

۳.۲ — جریان مطلوب (پس از راه‌حل)

۱. Client Request

ارسال درخواست با Header X-Api-Key

▼
۲. ForwardDefaultSelector اجرا می‌شود

وجود Header X-Api-Key را تشخیص می‌دهد و Scheme XApiKey را انتخاب می‌کند

▼
۳. XApiKeyAuthenticationHandler.HandleAuthenticateAsync

API Key را استخراج و ValidateApiKey را فراخوانی می‌کند

▼
۴. Claims تولید می‌شوند

Scopeهای واقعی از validationResult.Scopes به Claims اضافه می‌شوند

▼
۵. Policy-Based Authorization

Policyهای ApiKeyAccess و ApiKeyReadAccess با موفقیت بررسی می‌شوند

▼
✅ نتیجه: 200 OK + Response

دسترسی به Endpoint مورد نظر با موفقیت انجام می‌شود

۴

راه‌حل گام‌به‌گام

Step-by-Step Solution
۱
اصلاح ForwardDefaultSelector در Startup فایل: xIds/DI/XDIHelperExtension.cs

باید یک ForwardDefaultSelector تنظیم شود که بر اساس وجود Header X-Api-Key در Request، Scheme صحیح را انتخاب کند. این Selector برای تمام Schemeها (نه فقط JwtBearer) باید فعال باشد.

// xIds/DI/XDIHelperExtension.cs — بخش AddXIdentityServerAuthentication public static void AddXIdentityServerAuthentication( this IServiceCollection services, IConfiguration configuration, bool addApiKeyAuthentication = false ) { services.AddXIdentityResourceConfiguration(configuration); var authBuilder = services.AddAuthentication(); // ═══ ۱. ثبت Scheme برای XApiKey (اول از همه) if (addApiKeyAuthentication) { authBuilder.AddScheme<AuthenticationSchemeOptions, XApiKeyAuthenticationHandler>( XAuthenticationScheme.XApiKey.GetStringValue(), options => {} ); } // ═══ ۲. ثبت JwtBearer با ForwardDefaultSelector هوشمند authBuilder.AddJwtBearer(options => { options.SaveToken = true; options.RequireHTTPSMetadata = false; options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; // ═══ ۳. انتخاب Scheme بر اساس Header درخواست options.ForwardDefaultSelector = context => { // اگر Header X-Api-Key وجود دارد → از Scheme XApiKey استفاده کن var apiKeyHeader = XHeader.ApiKey.GetStringValue(); // "X-Api-Key" if (context.Request.Headers.ContainsKey(apiKeyHeader)) { return XAuthenticationScheme.XApiKey.GetStringValue(); } // در غیر این صورت، Scheme پیش‌فرض (LocalApi برای IdentityServer) return XAuthentication.IDENTITY_SERVER_LOCAL_API; }; }) .AddLocalApi(); }
💡 چرا این راه‌حل کار می‌کند؟ ForwardDefaultSelector یک delegate است که ASP.NET Core در هر درخواست فراخوانی می‌کند تا Scheme مناسب را تعیین کند. با بررسی وجود Header، ما به‌صورت پویا Scheme را انتخاب می‌کنیم.
۲
اصلاح XApiKeyAuthenticationHandler برای تولید Claims صحیح فایل: xIds/Providers/XApiKeyAuthenticationHandler.cs

Handler فعلی Scope ثابت "apikey" را اضافه می‌کند که با هیچ Policyای همخوانی ندارد. باید Scopeهای واقعی از validationResult.Scopes که در XApplicationProvider.ValidateApiKey استخراج شده‌اند، به Claims اضافه شوند.

// xIds/Providers/XApiKeyAuthenticationHandler.cs — نسخه اصلاح‌شده protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { // ۱. استخراج API Key از Header var apiKeyHeader = Options.HeaderName ?? XHeader.ApiKey.GetStringValue(); if (!Request.Headers.ContainsKey(apiKeyHeader)) { return AuthenticateResult.NoResult(); } var apiKey = Request.Headers[apiKeyHeader].ToString(); if (string.IsNullOrEmpty(apiKey)) { return AuthenticateResult.NoResult(); } // ۲. استخراج Client IP var clientIP = Request.HttpContext.Connection.RemoteIpAddress?.ToString(); // ۳. اعتبارسنجی API Key از طریق Provider var validationResult = await applicationProvider.ValidateApiKey( apiKey: apiKey, clientIP: clientIP ); if (validationResult.Errors.HasChild()) { var message = validationResult.Errors.ToListString('\n'); return AuthenticateResult.Fail(message); } // ═══ ۴. ساخت Claims — نکته کلیدی ═══ var claims = new List<Claim> { // شناسه کاربر مالک API Key new Claim(ClaimTypes.Name, validationResult.OwnerId), new Claim(JwtClaimTypes.Subject, validationResult.OwnerId), // شناسه Application new Claim("application_id", validationResult.ApplicationId.ToString()), // نوع احراز هویت برای تشخیص new Claim("auth_type", "apikey"), }; // ═══ ۵. اضافه کردن Scopeهای واقعی ═══ // این Scopeها توسط XApplicationProvider از AllowedScopes استخراج شده‌اند // و با Policyهای XAuthorizationHelper (read, write, admin, manage) همخوانی دارند if (validationResult.Scopes != null) { foreach (var scope in validationResult.Scopes) { claims.Add(new Claim(JwtClaimTypes.Scope, scope)); } } // ۶. ساخت Principal و Ticket var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); }
✅ نکته کلیدی: validationResult.Scopes مقادیری مانند read، write، admin، manage دارد که مستقیماً با Policyهای تعریف‌شده در XAuthorizationHelper مطابقت می‌کند.
۳
اطمینان از همخوانی Policyها با Scopeهای API Key فایل: xIdentityHelper/XAuthorizationHelper.cs

در XAuthorizationHelper، Policyهای مربوط به ApiKey تعریف شده‌اند. این Policyها باید با مقادیر XApiKeyScope در xIdentityModels/Constants/XApiKeyScope.cs همخوانی داشته باشند.

// xIdentityModels/Constants/XApiKeyScope.cs public enum XApiKeyScope { [StringValue("read")] Read, [StringValue("write")] Write, [StringValue("admin")] Admin, [StringValue("manage")] Manage }

و در XAuthorizationHelper:

// xIdentityHelper/XAuthorizationHelper.cs — Policyهای موجود ApiKey // ApiKey Access Policy — دسترسی به هر یک از Scopeها result.Add(XPolicies.ApiKeyAccess, new AuthorizationPolicyBuilder() .RequireClaim( JwtClaimTypes.Scope, XApiKeyScope.Read.GetStringValue(), // "read" XApiKeyScope.Write.GetStringValue(), // "write" XApiKeyScope.Manage.GetStringValue() // "manage" ) .Build() ); // ApiKey Read Access Policy result.Add(XPolicies.ApiKeyReadAccess, new AuthorizationPolicyBuilder() .RequireClaim(JwtClaimTypes.Scope, XApiKeyScope.Read.GetStringValue()) .Build() );
💡 نتیجه: اگر در گام ۲، Scopeهای واقعی از validationResult.Scopes به Claims اضافه شوند، این Policyها به‌صورت خودکار با موفقیت بررسی می‌شوند و دسترسی به Endpointهای محافظت‌شده امکان‌پذیر می‌گردد.
۴
استفاده صحیح در Controllerها نمونه استفاده صحیح

پس از اصلاحات بالا، برای محافظت از Endpointها با API Key، باید از Policyهای مربوطه استفاده کنید:

// نمونه Endpoint محافظت‌شده با API Key [RequireXPowered] [HttpPost("ApiKeys/Validate")] [Authorize(Policy = XPolicies.ApiKeyAccess)] // ← Policy مخصوص API Key public async Task<ActionResult<XApiKeyValidationResult>> ValidateApiKey( [FromBody] XApiKeyValidationRequest request, CancellationToken cancellationToken = default ) { // پیاده‌سازی }
⚠️ توجه مهم: از آنجایی که Policyهای ApiKey بر اساس Scope تعریف شده‌اند، نباید از Policyهای مبتنی بر Role مانند XPolicies.User یا XPolicies.EnabledUser استفاده کنید. این Policyها انتظار Role Claim دارند که API Key ندارد.
۵
تست و اعتبارسنجی Validation & Testing

پس از اعمال تغییرات، مسیر تست زیر را دنبال کنید:

گام ۵.۱ — ساخت یک API Key جدید

# Request: ساخت API Key جدید برای Application POST /Applications/{applicationId}/ApiKeys/Create Content-Type: application/json Authorization: Bearer {access_token} { "scopes": ["read", "write"], "rateLimit": 60, "expirationMinutes": 43200, "allowedIPs": ["127.0.0.1"] }

پاسخ باید شامل شیء XApiKeyDto باشد. نکته مهم: کلید اصلی (plainKey) فقط یک‌بار در پاسخ اولیه برگردانده می‌شود و باید آن را در جای امن ذخیره کنید.

گام ۵.۲ — تست Endpoint با API Key

# Request: تست Endpoint محافظت‌شده با API Key GET /Account/Test/HiApiKeyAccess X-Api-Key: xapp_xxxxxxxxxxxxxxxxxxxxxxxx X-PoweredBy: {xPoweredValue}

گام ۵.۳ — بررسی نتیجه

حالت پاسخ مورد انتظار تشخیص
API Key معتبر + Scope کافی 200 OK ✓ صحیح
API Key نامعتبر 401 Unauthorized با پیام «ApiKey not found» ✓ صحیح
API Key منقضی‌شده 401 با پیام «ApiKey is Expired» ✓ صحیح
Scope ناکافی 403 Forbidden ✓ صحیح
IP غیرمجاز 401 با پیام «Client IP not Allowed» ✓ صحیح
Rate Limit رد شده 401 با پیام «ApiKey Rate Limit Reached» ✓ صحیح
۶
به‌روزرسانی Endpoint «Validate API Key» اصلاح کنترلر موجود

در ApplicationsController+Custom.cs، متد ValidateApiKey فعلی از GetUserInfo() استفاده می‌کند که نیازمند احراز هویت با Bearer Token است. برای پیاده‌سازی درست، باید این Endpoint خودش با API Key احراز هویت شود (سناریوی self-validation):

// xIds/Controllers/Applications/ApplicationsController+Custom.cs // نسخه اصلاح‌شده — با Policy API Key [HttpPost("ApiKeys/Validate")] [AllowAnonymous] // ← چون خودش API Key را در Body می‌فرستد public async Task<ActionResult<XApiKeyValidationResult>> ValidateApiKey( [FromBody] XApiKeyValidationRequest request, CancellationToken cancellationToken = default ) { try { // اعتبارسنجی ورودی await ValidationProvider .GroupValidationBuilder() .AddNotNull(request) .AddNotEmpty(request.ApiKey) .ValidateGroupAsync(); // استخراج ClientIP از Connection واقعی var clientIP = HttpContext.Connection.RemoteIpAddress?.ToString(); // اعتبارسنجی مستقیم var result = await provider.ValidateApiKey( apiKey: request.ApiKey, clientIP: clientIP, requiredScope: request.RequiredScope, userInfo: null, // ← userInfo دیگر نیاز نیست cancellationToken: cancellationToken ); return Ok(result.ToDynamicObject()); } catch (Exception ex) { var result = GetExceptionActionResult(ex); return result; } }
💡 چرا AllowAnonymous؟ چون این Endpoint خودش نقش «اعتبارسنج API Key» را دارد و نباید قبل از رسیدن به آن، احراز هویت شود. منطق اعتبارسنجی درون خودش انجام می‌شود.
۵

خلاصه تغییرات

Summary of Changes
# فایل تغییر اولویت
۱ xIds/DI/XDIHelperExtension.cs افزودن ForwardDefaultSelector برای انتخاب پویا بین JwtBearer و XApiKey حیاتی
۲ xIds/Providers/XApiKeyAuthenticationHandler.cs حذف Scope ثابت «apikey» و افزودن Scopeهای واقعی از validationResult.Scopes حیاتی
۳ xIds/Startup.cs اطمینان از ترتیب صحیح ثبت Schemeها (نیاز به تغییر مستقیم ندارد اگر از Extension استفاده شود) متوسط
۴ xIds/Controllers/Applications/ApplicationsController+Custom.cs اصلاح ValidateApiKey برای کارکرد مستقل متوسط
۵ xIds/Controllers/Account/AccountController+Test.cs افزودن تست‌های احراز هویت با API Key (اختیاری، برای تست) توصیه‌شده
✅ نتیجه نهایی: با اعمال گام‌های ۱ و ۲ (که حیاتی هستند)، مشکل احراز هویت با XApiKey به‌طور کامل حل می‌شود. گام‌های ۳ تا ۵ برای بهبود و تکمیل توصیه می‌شوند.
۶

بهترین شیوه‌ها و توصیه‌های تکمیلی

Best Practices & Additional Recommendations

۶.۱ — مدیریت صحیح Scopeها در سطح API Key

هنگام ساخت API Key، از مقادیر XApiKeyScope استفاده کنید:

// نمونه ساخت API Key با Scopeهای صحیح var result = await provider.CreateApiKey( applicationId: applicationId, scopes: new[] { "read", "write" }, // ← مطابق XApiKeyScope allowedIPs: new[] { "192.168.1.100" }, rateLimit: 60, expiration: TimeSpan.FromDays(30), userInfo: userInfo );

۶.۲ — تفکیک Policyهای مبتنی بر API Key از Policyهای مبتنی بر Role

Policyهای موجود در XPolicies دو دسته هستند:

  • Policyهای مبتنی بر Scope (مثل ApiKeyAccess, ReadAccess) — برای API Key و Bearer Token
  • Policyهای مبتنی بر Role (مثل User, Admin) — فقط برای Bearer Token
⚠️ توجه: هرگز از Policyهای Role-based برای Endpointهایی که قرار است با API Key محافظت شوند استفاده نکنید. این Policyها انتظار Claim از نوع role دارند که API Key ندارد.

۶.۳ — ذخیره امن Plain Key

کلید اصلی (plain key) فقط در لحظه ساخت برگردانده می‌شود. باید در جای امن ذخیره شود. در صورتی که کاربر آن را گم کند، باید کلید قبلی revoke شده و کلید جدید ساخته شود.

۶.۴ — Rate Limit و Audit Log

در XApiKeyConfiguration، ویژگی‌های زیر فعال هستند:

  • DefaultRateLimit — محدودیت نرخ پیش‌فرض
  • EnableAuditLog — برای ثبت استفاده از API Keyها

توصیه می‌شود در ValidateApiKey پس از اعتبارسنجی موفق، یک رکورد XApiKeyUsage ثبت کنید تا تاریخچه استفاده قابل ردیابی باشد.

۶.۵ — Endpointهای توصیه‌شده برای تست

برای اطمینان از کارکرد صحیح، این Endpointها را در AccountController+Test.cs اضافه کنید:

[RequireXPowered] [HttpGet("Test/HiApiKeyReadAccess")] [Authorize(Policy = XPolicies.ApiKeyReadAccess)] public ActionResult<string> HiApiKeyReadAccess() { return Ok("API Key Read Access Passed ..."); } [RequireXPowered] [HttpPost("Test/HiApiKeyWriteAccess")] [Authorize(Policy = XPolicies.ApiKeyWriteAccess)] public ActionResult<string> HiApiKeyWriteAccess() { return Ok("API Key Write Access Passed ..."); }
۷

جمع‌بندی

Conclusion

مشکل عدم احراز هویت با XApiKey در xIds ریشه در دو نقطه کد کلیدی دارد که هر دو در این سند شناسایی و راه‌حل آن‌ها ارائه شد:

  1. عدم انتخاب صحیح Scheme در Startup — که با افزودن ForwardDefaultSelector در AddJwtBearer حل می‌شود.
  2. عدم تطابق Claims تولیدشده با Policyها — که با افزودن Scopeهای واقعی از validationResult.Scopes در XApiKeyAuthenticationHandler حل می‌شود.

با اعمال این دو تغییر ساده اما حیاتی، سیستم احراز هویت مبتنی بر API Key به‌طور کامل فعال می‌شود و Endpointهای محافظت‌شده با Policyهای ApiKeyAccess، ApiKeyReadAccess، ApiKeyWriteAccess و ApiKeyAdminAccess به درستی کار می‌کنند.

✅ کلید موفقیت: زیرساخت موجود در xIds برای مدیریت API Key بسیار کامل و حرفه‌ای است. تنها دو نقطه اتصال کوچک در لایه احراز هویت نیاز به اصلاح داشتند که در این سند به‌طور کامل تشریح شدند.
این مستند فنی محرمانه بوده و صرفاً جهت استفاده تیم توسعه xSaherelmWorkspace تهیه شده است.