Files
xSaherelmWorkspace/Documents/Docs/Developer/Adding xApiKey Validation 1.html
T
2026-10-10 16:24:11 +03:30

1667 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تحلیل و رفع مشکل احراز هویت XApiKey در xIds</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--primary: #0f2a4a;
--primary-light: #1e4976;
--primary-dark: #081a2e;
--accent: #c9a961;
--accent-light: #e0c78a;
--accent-dark: #a8893f;
--bg: #faf9f6;
--card-bg: #ffffff;
--text: #1a202c;
--text-muted: #4a5568;
--border: #e2e8f0;
--success: #2f855a;
--success-light: #48bb78;
--danger: #c53030;
--danger-light: #fc8181;
--warning: #c05621;
--warning-light: #f6ad55;
--info: #2c5282;
--code-bg: #1a202c;
--code-text: #e2e8f0;
--shadow: 0 4px 6px rgba(0,0,0,0.05), 0 10px 30px rgba(15,42,74,0.08);
--shadow-lg: 0 10px 20px rgba(0,0,0,0.08), 0 20px 40px rgba(15,42,74,0.12);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Vazirmatn', Tahoma, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.9;
font-size: 15px;
padding: 30px 15px;
}
.document-container {
max-width: 1000px;
margin: 0 auto;
background: var(--card-bg);
box-shadow: var(--shadow-lg);
border-radius: 16px;
overflow: hidden;
}
/* ─── HEADER ─── */
.document-header {
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 50%, var(--primary-light) 100%);
color: white;
padding: 55px 45px 45px;
position: relative;
border-bottom: 4px solid var(--accent);
overflow: hidden;
}
.document-header::before {
content: '';
position: absolute;
top: -100px; left: -100px;
width: 400px; height: 400px;
background: radial-gradient(circle, rgba(201,169,97,0.18) 0%, transparent 70%);
border-radius: 50%;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
position: relative;
z-index: 1;
flex-wrap: wrap;
gap: 20px;
}
.company-badge {
display: flex;
align-items: center;
gap: 15px;
}
.company-logo {
width: 60px; height: 60px;
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
border-radius: 14px;
box-shadow: 0 8px 20px rgba(201,169,97,0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
font-weight: 900;
color: white;
}
.company-name {
font-size: 19px;
font-weight: 800;
}
.company-tagline {
font-size: 12px;
opacity: 0.75;
}
.doc-meta {
text-align: left;
font-size: 12px;
opacity: 0.85;
}
.doc-meta div { margin-bottom: 4px; }
.doc-meta .meta-label { color: var(--accent-light); font-weight: 600; }
.document-title {
text-align: center;
position: relative;
z-index: 1;
}
.document-title .label {
display: inline-block;
background: rgba(201,169,97,0.18);
color: var(--accent-light);
padding: 5px 18px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
margin-bottom: 16px;
border: 1px solid rgba(201,169,97,0.4);
letter-spacing: 2px;
}
.document-title h1 {
font-size: 28px;
font-weight: 900;
margin-bottom: 12px;
letter-spacing: -0.5px;
line-height: 1.4;
}
.document-title .subtitle {
font-size: 15px;
opacity: 0.85;
max-width: 700px;
margin: 0 auto;
}
/* ─── BODY ─── */
.document-body {
padding: 50px;
}
.intro-text {
background: linear-gradient(to left, #f7fafc, #edf2f7);
border-right: 4px solid var(--accent);
padding: 22px 28px;
border-radius: 10px;
margin-bottom: 40px;
font-size: 14.5px;
color: var(--text-muted);
}
.intro-text strong { color: var(--primary); }
/* ─── SECTIONS ─── */
.section {
margin-bottom: 45px;
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 14px;
border-bottom: 2px solid var(--border);
margin-bottom: 22px;
}
.section-number {
width: 52px; height: 52px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
border-radius: 13px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 20px;
flex-shrink: 0;
box-shadow: 0 6px 14px rgba(15,42,74,0.25);
}
.section-number.danger {
background: linear-gradient(135deg, var(--danger), #9b2c2c);
}
.section-number.success {
background: linear-gradient(135deg, var(--success), #22543d);
}
.section-title { flex: 1; }
.section-title h3 {
font-size: 20px;
font-weight: 800;
color: var(--primary);
margin-bottom: 2px;
}
.section-title .subtitle {
font-size: 12px;
color: var(--text-muted);
}
.section-content { padding-right: 8px; }
.section-content p {
margin-bottom: 14px;
text-align: justify;
}
.section-content h4 {
font-size: 16px;
font-weight: 700;
color: var(--primary);
margin: 22px 0 12px;
}
.section-content ul, .section-content ol {
padding-right: 24px;
margin: 12px 0;
}
.section-content li {
margin-bottom: 8px;
line-height: 1.85;
}
/* ─── BOXES ─── */
.highlight-box, .info-box, .success-box, .warning-box, .danger-box {
padding: 20px 24px;
border-radius: 10px;
margin: 22px 0;
font-size: 14.5px;
}
.highlight-box {
background: linear-gradient(to left, #fffaf0, #fef5e7);
border: 1px solid #f6e05e;
border-right: 4px solid var(--accent);
}
.highlight-box strong { color: var(--accent-dark); }
.info-box {
background: linear-gradient(to left, #ebf8ff, #e6f6ff);
border: 1px solid #90cdf4;
border-right: 4px solid var(--info);
}
.success-box {
background: linear-gradient(to left, #f0fff4, #e6fffa);
border: 1px solid #9ae6b4;
border-right: 4px solid var(--success);
}
.warning-box {
background: linear-gradient(to left, #fffaf0, #fef5e7);
border: 1px solid #fbd38d;
border-right: 4px solid var(--warning);
}
.danger-box {
background: linear-gradient(to left, #fff5f5, #fed7d7);
border: 1px solid #fc8181;
border-right: 4px solid var(--danger);
}
.danger-box strong { color: var(--danger); }
/* ─── CODE ─── */
.code-block {
background: var(--code-bg);
color: var(--code-text);
border-radius: 10px;
padding: 20px 24px;
margin: 16px 0;
overflow-x: auto;
direction: ltr;
text-align: left;
font-family: 'JetBrains Mono', 'Consolas', monospace;
font-size: 13px;
line-height: 1.7;
position: relative;
border: 1px solid #2d3748;
}
.code-block::before {
content: attr(data-lang);
position: absolute;
top: 8px;
right: 12px;
font-size: 10px;
color: var(--accent);
font-weight: 600;
letter-spacing: 1px;
direction: ltr;
}
.code-block .keyword { color: #f687b3; }
.code-block .type { color: #63b3ed; }
.code-block .string { color: #9ae6b4; }
.code-block .comment { color: #718096; font-style: italic; }
.code-block .method { color: #f6e05e; }
.code-block .number { color: #fbb6ce; }
code {
background: #edf2f7;
color: var(--primary);
padding: 2px 8px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
direction: ltr;
display: inline-block;
}
/* ─── TABLES ─── */
.styled-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: var(--shadow);
font-size: 13.5px;
}
.styled-table thead {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
}
.styled-table th {
padding: 14px 16px;
text-align: right;
font-weight: 600;
font-size: 13px;
}
.styled-table td {
padding: 13px 16px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.styled-table tbody tr:last-child td { border-bottom: none; }
.styled-table tbody tr:nth-child(even) { background: #f8fafc; }
.styled-table code {
font-size: 12px;
padding: 1px 6px;
}
/* ─── STEPS ─── */
.step-card {
background: white;
border: 2px solid var(--border);
border-radius: 12px;
padding: 25px 28px;
margin-bottom: 20px;
position: relative;
transition: all 0.3s;
}
.step-card:hover {
border-color: var(--accent);
box-shadow: var(--shadow);
}
.step-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 15px;
}
.step-number {
width: 44px; height: 44px;
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 18px;
flex-shrink: 0;
box-shadow: 0 6px 14px rgba(201,169,97,0.3);
}
.step-title {
font-size: 17px;
font-weight: 800;
color: var(--primary);
}
.step-title .step-subtitle {
font-size: 12px;
font-weight: 400;
color: var(--text-muted);
display: block;
margin-top: 2px;
}
.step-body p {
margin-bottom: 12px;
}
/* ─── ISSUE CARD ─── */
.issue-card {
background: linear-gradient(to left, #fff5f5, #fed7d7);
border: 2px solid #fc8181;
border-radius: 12px;
padding: 25px;
margin: 20px 0;
}
.issue-card .issue-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 17px;
font-weight: 800;
color: var(--danger);
margin-bottom: 15px;
}
.issue-card .issue-icon {
font-size: 24px;
}
/* ─── FILE REF ─── */
.file-ref {
display: inline-flex;
align-items: center;
gap: 6px;
background: #edf2f7;
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px 10px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--primary);
direction: ltr;
margin: 2px 0;
}
/* ─── DIAGNOSIS ─── */
.diagnosis-tree {
background: linear-gradient(135deg, #f8fafc, #edf2f7);
border: 2px solid var(--border);
border-radius: 12px;
padding: 25px;
margin: 20px 0;
}
.diagnosis-item {
display: flex;
gap: 15px;
padding: 15px 0;
border-bottom: 1px dashed var(--border);
}
.diagnosis-item:last-child { border-bottom: none; }
.diagnosis-badge {
flex-shrink: 0;
width: 32px; height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 14px;
color: white;
}
.diagnosis-badge.fail { background: var(--danger); }
.diagnosis-badge.pass { background: var(--success); }
.diagnosis-badge.warn { background: var(--warning); }
.diagnosis-content { flex: 1; }
.diagnosis-content h5 {
font-size: 14.5px;
font-weight: 700;
color: var(--primary);
margin-bottom: 6px;
}
.diagnosis-content p {
font-size: 13.5px;
color: var(--text-muted);
margin: 0;
}
/* ─── DIAGRAM ─── */
.flow-diagram {
background: white;
border: 2px solid var(--border);
border-radius: 12px;
padding: 30px 20px;
margin: 20px 0;
overflow-x: auto;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 800px;
margin: 0 auto;
}
.flow-step-row {
display: flex;
align-items: center;
gap: 12px;
}
.flow-step-box {
flex: 1;
background: #f8fafc;
border: 2px solid var(--border);
border-radius: 10px;
padding: 14px 18px;
position: relative;
}
.flow-step-box.fail {
background: linear-gradient(to left, #fff5f5, #fed7d7);
border-color: #fc8181;
}
.flow-step-box.success {
background: linear-gradient(to left, #f0fff4, #e6fffa);
border-color: #9ae6b4;
}
.flow-step-box h5 {
font-size: 13.5px;
font-weight: 700;
color: var(--primary);
margin-bottom: 4px;
}
.flow-step-box p {
font-size: 12.5px;
color: var(--text-muted);
margin: 0;
line-height: 1.7;
}
.flow-arrow-down {
text-align: center;
font-size: 20px;
color: var(--accent);
margin: 2px 0;
}
/* ─── BADGES ─── */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 10px;
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.5px;
}
.badge-fail { background: #fed7d7; color: var(--danger); }
.badge-pass { background: #c6f6d5; color: var(--success); }
.badge-warn { background: #feebc8; color: var(--warning); }
.badge-info { background: #bee3f8; color: var(--info); }
/* ─── FOOTER ─── */
.document-footer {
background: var(--primary-dark);
color: white;
padding: 30px 40px;
text-align: center;
font-size: 12px;
}
.document-footer .footer-brand {
font-weight: 800;
color: var(--accent);
margin-bottom: 8px;
font-size: 15px;
}
.footer-divider {
width: 80px;
height: 2px;
background: var(--accent);
margin: 14px auto;
border-radius: 2px;
}
.page-info {
text-align: center;
padding: 18px;
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
background: #f8fafc;
}
@media print {
body { background: white; padding: 0; }
.document-container { box-shadow: none; border-radius: 0; }
}
@media (max-width: 768px) {
.document-body { padding: 30px 22px; }
.document-header { padding: 35px 25px; }
.header-top { flex-direction: column; }
.doc-meta { text-align: right; }
.document-title h1 { font-size: 22px; }
.flow-step-row { flex-direction: column; }
}
</style>
</head>
<body>
<div class="document-container">
<!-- ══════════ HEADER ══════════ -->
<header class="document-header">
<div class="header-top">
<div class="company-badge">
<div class="company-logo">X</div>
<div>
<div class="company-name">xSaherelmWorkspace</div>
<div class="company-tagline">Technical Analysis Document</div>
</div>
</div>
<div class="doc-meta">
<div><span class="meta-label">شماره مستند:</span> XIDS-AUTH-2026-001</div>
<div><span class="meta-label">نسخه:</span> ۱.۰</div>
<div><span class="meta-label">موضوع:</span> رفع مشکل احراز هویت XApiKey</div>
<div><span class="meta-label">طبقه‌بندی:</span> داخلی — فنی</div>
</div>
</div>
<div class="document-title">
<span class="label">TECHNICAL DIAGNOSIS & SOLUTION</span>
<h1>ریشه‌یابی و رفع مشکل احراز هویت با XApiKey در xIds</h1>
<div class="subtitle">
تحلیل جامع کد، شناسایی گلوگاه احراز هویت، و ارائه راه‌حل گام‌به‌گام برای فعال‌سازی کامل
Authentication مبتنی بر API Key در سرور IdentityServer4
</div>
</div>
</header>
<!-- ══════════ BODY ══════════ -->
<div class="document-body">
<div class="intro-text">
<strong>خلاصه اجرایی:</strong><br>
پس از بررسی کامل کدهای پروژه <strong>xIds</strong>، مشخص شد که زیرساخت مدیریت Application و
تولید <code>XApiKey</code> به‌درستی پیاده‌سازی شده است، اما <strong>زنجیره احراز هویت مبتنی بر API Key
در سمت سرور IdentityServer4 ناقص مانده است</strong>. این سند ابتدا ریشه مشکل را با استناد به کدهای
موجود تشریح می‌کند، سپس راه‌حل کامل گام‌به‌گام را ارائه می‌دهد.
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 1: CURRENT STATE -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number">۱</div>
<div class="section-title">
<h3>وضعیت فعلی سیستم</h3>
<div class="subtitle">Current State Analysis</div>
</div>
</div>
<div class="section-content">
<p>
در پروژه xIds، اجزای زیر برای مدیریت Application و API Key پیاده‌سازی شده‌اند:
</p>
<h4>۱.۱ — اجزای پیاده‌سازی‌شده</h4>
<table class="styled-table">
<thead>
<tr>
<th>جزء</th>
<th>مسیر فایل</th>
<th>وضعیت</th>
</tr>
</thead>
<tbody>
<tr>
<td>مدل‌های Application و ApiKey</td>
<td><span class="file-ref">xIdentityModels/Models/XApplication.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
<tr>
<td>XApplicationProvider (مدیریت API Key)</td>
<td><span class="file-ref">xIds/Providers/XApplicationProvider.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
<tr>
<td>XApiKeyHelper (تولید و هش)</td>
<td><span class="file-ref">xIds/Helpers/XApiKeyHelper.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
<tr>
<td>ApplicationsController (Endpointها)</td>
<td><span class="file-ref">xIds/Controllers/ApplicationsController.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
<tr>
<td>XApiKeyAuthenticationHandler</td>
<td><span class="file-ref">xIds/Providers/XApiKeyAuthenticationHandler.cs</span></td>
<td><span class="badge badge-warn">⚠ موجود اما ناقص</span></td>
</tr>
<tr>
<td>XApiKeyConfiguration</td>
<td><span class="file-ref">xIds/Configurations/XApiKeyConfiguration.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
<tr>
<td>ثبت Scheme در Startup</td>
<td><span class="file-ref">xIds/Startup.cs</span></td>
<td><span class="badge badge-pass">✓ موجود</span></td>
</tr>
</tbody>
</table>
<h4>۱.۲ — معماری جاری احراز هویت در Startup.cs</h4>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/Startup.cs</span>
services.<span class="method">AddXIdentityServerAuthentication</span>(
configuration: Configuration,
addApiKeyAuthentication: <span class="keyword">true</span> <span class="comment">// ← این خط فعال است</span>
);
</div>
<p>
و در <code>XDIHelperExtension.AddXIdentityServerAuthentication</code>:
</p>
<div class="code-block" data-lang="C#">
<span class="keyword">var</span> authBuilder = services.<span class="method">AddAuthentication</span>();
authBuilder.<span class="method">AddJwtBearer</span>(options =&gt;
{
options.SaveToken = <span class="keyword">true</span>;
options.RequireHttpsMetadata = <span class="keyword">false</span>;
options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API;
})
.<span class="method">AddLocalApi</span>();
<span class="keyword">if</span> (addApiKeyAuthentication)
{
authBuilder.<span class="method">AddScheme</span>&lt;AuthenticationSchemeOptions, XApiKeyAuthenticationHandler&gt;(
XAuthenticationScheme.XApiKey.<span class="method">GetStringValue</span>(), <span class="comment">// "XApiKey"</span>
options =&gt; {}
);
}
</div>
<div class="info-box">
<strong>نتیجه:</strong> به نظر می‌رسد همه چیز در Startup به‌درستی ثبت شده است. اما در عمل، درخواست‌هایی
که Header <code>X-Api-Key</code> دارند، احراز هویت نمی‌شوند. علت چیست؟
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 2: ROOT CAUSE -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number danger">۲</div>
<div class="section-title">
<h3>ریشه‌یابی مشکل</h3>
<div class="subtitle">Root Cause Analysis</div>
</div>
</div>
<div class="section-content">
<p>
پس از بررسی دقیق کدها، <strong>سه ریشه اصلی</strong> برای عدم کارکرد احراز هویت با XApiKey شناسایی شد:
</p>
<!-- Issue 1 -->
<div class="issue-card">
<div class="issue-title">
<span class="issue-icon">🔴</span>
مشکل ۱: ForwardDefaultSelector به XApiKey توجه نمی‌کند
</div>
<p>
در <code>AddJwtBearer</code>، تنظیم <code>ForwardDefault</code> روی
<code>XAuthentication.IDENTITY_SERVER_LOCAL_API</code> قرار داده شده است. این یعنی
ASP.NET Core همیشه از Scheme پیش‌فرض (<code>IdentityServerAccessToken</code>) استفاده می‌کند
و هیچ‌گاه به Scheme با نام <code>XApiKey</code> نمی‌رسد.
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/DI/XDIHelperExtension.cs — AddXIdentityServerAuthentication</span>
authBuilder.<span class="method">AddJwtBearer</span>(options =&gt;
{
options.SaveToken = <span class="keyword">true</span>;
options.RequireHttpsMetadata = <span class="keyword">false</span>;
options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API; <span class="comment">// ← مشکل اصلی اینجاست</span>
})
</div>
</div>
<!-- Issue 2 -->
<div class="issue-card">
<div class="issue-title">
<span class="issue-icon">🔴</span>
مشکل ۲: ترتیب ثبت Schemeها اشتباه است
</div>
<p>
در ASP.NET Core، <strong>اولین Scheme ثبت‌شده، Scheme پیش‌فرض است</strong>. در کد فعلی،
<code>AddJwtBearer</code> قبل از <code>AddScheme&lt;...XApiKeyAuthenticationHandler&gt;</code> ثبت
می‌شود و این باعث می‌شود حتی اگر Header <code>X-Api-Key</code> ارسال شود، Handler مربوطه
هرگز فراخوانی نگردد.
</p>
</div>
<!-- Issue 3 -->
<div class="issue-card">
<div class="issue-title">
<span class="issue-icon">🔴</span>
مشکل ۳: XApiKeyAuthenticationHandler به Claimهای Policyهای موجود پاسخ نمی‌دهد
</div>
<p>
Policyهای موجود در <code>XAuthorizationHelper</code> (مانند <code>ApiKeyAccess</code> و
<code>ApiKeyReadAccess</code>) انتظار Claim از نوع <code>JwtClaimTypes.Scope</code> با مقادیر
<code>read</code>، <code>write</code>، <code>admin</code> یا <code>manage</code> دارند.
اما <code>XApiKeyAuthenticationHandler</code> فعلی در کد، یک Scope ثابت به نام
<code>"apikey"</code> اضافه می‌کند که با هیچ Policy موجودی مطابقت ندارد.
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/Providers/XApiKeyAuthenticationHandler.cs — کد فعلی</span>
<span class="keyword">var</span> claims = <span class="keyword">new</span>[]
{
<span class="keyword">new</span> <span class="type">Claim</span>(ClaimTypes.Name, validationResult.OwnerId),
<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"application_id"</span>, validationResult.ApplicationId.<span class="method">ToString</span>()),
<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"auth_type"</span>, <span class="string">"apikey"</span>),
<span class="keyword">new</span> <span class="type">Claim</span>(JwtClaimTypes.Scope, <span class="string">"apikey"</span>), <span class="comment">// ← هیچ Policyای این Scope را نمی‌شناسد</span>
};
<span class="comment">// سپس scopes از validationResult اضافه می‌شود، اما با مقادیر "read", "write"...</span>
<span class="comment">// که با Policyهای XAuthorizationHelper همخوانی ندارند</span>
</div>
</div>
<h4>۲.۱ — درخت عیب‌یابی</h4>
<div class="diagnosis-tree">
<div class="diagnosis-item">
<div class="diagnosis-badge fail">✗</div>
<div class="diagnosis-content">
<h5>درخواست با Header: X-Api-Key: xapp_xxxxx</h5>
<p>Header به درستی در Request وجود دارد.</p>
</div>
</div>
<div class="diagnosis-item">
<div class="diagnosis-badge fail">✗</div>
<div class="diagnosis-content">
<h5>ASP.NET Core می‌خواهد Scheme پیش‌فرض را انتخاب کند</h5>
<p>به دلیل <code>ForwardDefault = IDENTITY_SERVER_LOCAL_API</code>، همیشه Scheme پیش‌فرض انتخاب می‌شود.</p>
</div>
</div>
<div class="diagnosis-item">
<div class="diagnosis-badge fail">✗</div>
<div class="diagnosis-content">
<h5>XApiKeyAuthenticationHandler فراخوانی نمی‌شود</h5>
<p>چون هیچ‌گاه انتخاب نمی‌شود، متد HandleAuthenticateAsync اجرا نمی‌گردد.</p>
</div>
</div>
<div class="diagnosis-item">
<div class="diagnosis-badge fail">✗</div>
<div class="diagnosis-content">
<h5>درخواست با خطای 401 Unauthorized رد می‌شود</h5>
<p>پایان مسیر — احراز هویت شکست خورده است.</p>
</div>
</div>
</div>
<div class="danger-box">
<strong>⚠️ خلاصه ریشه مشکل:</strong> در معماری فعلی، ASP.NET Core با
<code>ForwardDefault</code> به Scheme پیش‌فرض هدایت می‌شود و هرگز به Scheme
<code>XApiKey</code> نمی‌رسد. علاوه بر این، حتی اگر Handler فراخوانی شود، Claimهای
تولیدشده با Policyهای موجود همخوانی ندارند و در نتیجه Authorization نیز شکست می‌خورد.
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 3: FLOW DIAGRAM -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number">۳</div>
<div class="section-title">
<h3>نمودار جریان فعلی در مقابل جریان مطلوب</h3>
<div class="subtitle">Current vs. Desired Auth Flow</div>
</div>
</div>
<div class="section-content">
<h4>۳.۱ — جریان فعلی (معیوب)</h4>
<div class="flow-diagram">
<div class="flow-steps">
<div class="flow-step-row">
<div class="flow-step-box">
<h5>۱. Client Request</h5>
<p>ارسال درخواست با Header <code>X-Api-Key</code></p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box fail">
<h5>۲. Default Scheme Selection</h5>
<p>ASP.NET Core به دلیل ForwardDefault، Scheme پیش‌فرض را انتخاب می‌کند</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box fail">
<h5>۳. JwtBearer Handler اجرا می‌شود</h5>
<p>به‌دنبال Bearer Token می‌گردد، پیدا نمی‌کند، شکست می‌خورد</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box fail">
<h5>❌ نتیجه: 401 Unauthorized</h5>
<p>XApiKeyAuthenticationHandler هرگز اجرا نمی‌شود</p>
</div>
</div>
</div>
</div>
<h4>۳.۲ — جریان مطلوب (پس از راه‌حل)</h4>
<div class="flow-diagram">
<div class="flow-steps">
<div class="flow-step-row">
<div class="flow-step-box">
<h5>۱. Client Request</h5>
<p>ارسال درخواست با Header <code>X-Api-Key</code></p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box success">
<h5>۲. ForwardDefaultSelector اجرا می‌شود</h5>
<p>وجود Header <code>X-Api-Key</code> را تشخیص می‌دهد و Scheme <code>XApiKey</code> را انتخاب می‌کند</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box success">
<h5>۳. XApiKeyAuthenticationHandler.HandleAuthenticateAsync</h5>
<p>API Key را استخراج و <code>ValidateApiKey</code> را فراخوانی می‌کند</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box success">
<h5>۴. Claims تولید می‌شوند</h5>
<p>Scopeهای واقعی از <code>validationResult.Scopes</code> به Claims اضافه می‌شوند</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box success">
<h5>۵. Policy-Based Authorization</h5>
<p>Policyهای <code>ApiKeyAccess</code> و <code>ApiKeyReadAccess</code> با موفقیت بررسی می‌شوند</p>
</div>
</div>
<div class="flow-arrow-down">▼</div>
<div class="flow-step-row">
<div class="flow-step-box success">
<h5>✅ نتیجه: 200 OK + Response</h5>
<p>دسترسی به Endpoint مورد نظر با موفقیت انجام می‌شود</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 4: SOLUTION -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number success">۴</div>
<div class="section-title">
<h3>راه‌حل گام‌به‌گام</h3>
<div class="subtitle">Step-by-Step Solution</div>
</div>
</div>
<div class="section-content">
<!-- STEP 1 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۱</div>
<div class="step-title">
اصلاح ForwardDefaultSelector در Startup
<span class="step-subtitle">فایل: xIds/DI/XDIHelperExtension.cs</span>
</div>
</div>
<div class="step-body">
<p>
باید یک <code>ForwardDefaultSelector</code> تنظیم شود که بر اساس وجود Header
<code>X-Api-Key</code> در Request، Scheme صحیح را انتخاب کند. این Selector
برای <strong>تمام Schemeها</strong> (نه فقط JwtBearer) باید فعال باشد.
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/DI/XDIHelperExtension.cs — بخش AddXIdentityServerAuthentication</span>
<span class="keyword">public static void</span> <span class="method">AddXIdentityServerAuthentication</span>(
<span class="keyword">this</span> <span class="type">IServiceCollection</span> services,
<span class="type">IConfiguration</span> configuration,
<span class="keyword">bool</span> addApiKeyAuthentication = <span class="keyword">false</span>
)
{
services.<span class="method">AddXIdentityResourceConfiguration</span>(configuration);
<span class="keyword">var</span> authBuilder = services.<span class="method">AddAuthentication</span>();
<span class="comment">// ═══ ۱. ثبت Scheme برای XApiKey (اول از همه)</span>
<span class="keyword">if</span> (addApiKeyAuthentication)
{
authBuilder.<span class="method">AddScheme</span>&lt;<span class="type">AuthenticationSchemeOptions</span>, <span class="type">XApiKeyAuthenticationHandler</span>&gt;(
XAuthenticationScheme.XApiKey.<span class="method">GetStringValue</span>(),
options =&gt; {}
);
}
<span class="comment">// ═══ ۲. ثبت JwtBearer با ForwardDefaultSelector هوشمند</span>
authBuilder.<span class="method">AddJwtBearer</span>(options =&gt;
{
options.SaveToken = <span class="keyword">true</span>;
options.RequireHTTPSMetadata = <span class="keyword">false</span>;
options.ForwardDefault = XAuthentication.IDENTITY_SERVER_LOCAL_API;
<span class="comment">// ═══ ۳. انتخاب Scheme بر اساس Header درخواست</span>
options.ForwardDefaultSelector = context =&gt;
{
<span class="comment">// اگر Header X-Api-Key وجود دارد → از Scheme XApiKey استفاده کن</span>
<span class="keyword">var</span> apiKeyHeader = XHeader.ApiKey.<span class="method">GetStringValue</span>(); <span class="comment">// "X-Api-Key"</span>
<span class="keyword">if</span> (context.Request.Headers.<span class="method">ContainsKey</span>(apiKeyHeader))
{
<span class="keyword">return</span> XAuthenticationScheme.XApiKey.<span class="method">GetStringValue</span>();
}
<span class="comment">// در غیر این صورت، Scheme پیش‌فرض (LocalApi برای IdentityServer)</span>
<span class="keyword">return</span> XAuthentication.IDENTITY_SERVER_LOCAL_API;
};
})
.<span class="method">AddLocalApi</span>();
}
</div>
<div class="info-box">
<strong>💡 چرا این راه‌حل کار می‌کند؟</strong> <code>ForwardDefaultSelector</code>
یک delegate است که ASP.NET Core در هر درخواست فراخوانی می‌کند تا Scheme مناسب را
تعیین کند. با بررسی وجود Header، ما به‌صورت پویا Scheme را انتخاب می‌کنیم.
</div>
</div>
</div>
<!-- STEP 2 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۲</div>
<div class="step-title">
اصلاح XApiKeyAuthenticationHandler برای تولید Claims صحیح
<span class="step-subtitle">فایل: xIds/Providers/XApiKeyAuthenticationHandler.cs</span>
</div>
</div>
<div class="step-body">
<p>
Handler فعلی Scope ثابت <code>"apikey"</code> را اضافه می‌کند که با هیچ Policyای
همخوانی ندارد. باید Scopeهای واقعی از <code>validationResult.Scopes</code>
که در <code>XApplicationProvider.ValidateApiKey</code> استخراج شده‌اند، به Claims
اضافه شوند.
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/Providers/XApiKeyAuthenticationHandler.cs — نسخه اصلاح‌شده</span>
<span class="keyword">protected override async</span> <span class="type">Task</span>&lt;<span class="type">AuthenticateResult</span>&gt; <span class="method">HandleAuthenticateAsync</span>()
{
<span class="comment">// ۱. استخراج API Key از Header</span>
<span class="keyword">var</span> apiKeyHeader = <span class="method">Options</span>.HeaderName ?? XHeader.ApiKey.<span class="method">GetStringValue</span>();
<span class="keyword">if</span> (!Request.Headers.<span class="method">ContainsKey</span>(apiKeyHeader))
{
<span class="keyword">return</span> AuthenticateResult.<span class="method">NoResult</span>();
}
<span class="keyword">var</span> apiKey = Request.Headers[apiKeyHeader].<span class="method">ToString</span>();
<span class="keyword">if</span> (<span class="keyword">string</span>.<span class="method">IsNullOrEmpty</span>(apiKey))
{
<span class="keyword">return</span> AuthenticateResult.<span class="method">NoResult</span>();
}
<span class="comment">// ۲. استخراج Client IP</span>
<span class="keyword">var</span> clientIP = Request.HttpContext.Connection.RemoteIpAddress?.<span class="method">ToString</span>();
<span class="comment">// ۳. اعتبارسنجی API Key از طریق Provider</span>
<span class="keyword">var</span> validationResult = <span class="keyword">await</span> applicationProvider.<span class="method">ValidateApiKey</span>(
apiKey: apiKey,
clientIP: clientIP
);
<span class="keyword">if</span> (validationResult.Errors.<span class="method">HasChild</span>())
{
<span class="keyword">var</span> message = validationResult.Errors.<span class="method">ToListString</span>(<span class="string">'\n'</span>);
<span class="keyword">return</span> AuthenticateResult.<span class="method">Fail</span>(message);
}
<span class="comment">// ═══ ۴. ساخت Claims — نکته کلیدی ═══</span>
<span class="keyword">var</span> claims = <span class="keyword">new</span> <span class="type">List</span>&lt;<span class="type">Claim</span>&gt;
{
<span class="comment">// شناسه کاربر مالک API Key</span>
<span class="keyword">new</span> <span class="type">Claim</span>(<span class="type">ClaimTypes</span>.Name, validationResult.OwnerId),
<span class="keyword">new</span> <span class="type">Claim</span>(JwtClaimTypes.Subject, validationResult.OwnerId),
<span class="comment">// شناسه Application</span>
<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"application_id"</span>, validationResult.ApplicationId.<span class="method">ToString</span>()),
<span class="comment">// نوع احراز هویت برای تشخیص</span>
<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"auth_type"</span>, <span class="string">"apikey"</span>),
};
<span class="comment">// ═══ ۵. اضافه کردن Scopeهای واقعی ═══</span>
<span class="comment">// این Scopeها توسط XApplicationProvider از AllowedScopes استخراج شده‌اند</span>
<span class="comment">// و با Policyهای XAuthorizationHelper (read, write, admin, manage) همخوانی دارند</span>
<span class="keyword">if</span> (validationResult.Scopes != <span class="keyword">null</span>)
{
<span class="keyword">foreach</span> (<span class="keyword">var</span> scope <span class="keyword">in</span> validationResult.Scopes)
{
claims.<span class="method">Add</span>(<span class="keyword">new</span> <span class="type">Claim</span>(JwtClaimTypes.Scope, scope));
}
}
<span class="comment">// ۶. ساخت Principal و Ticket</span>
<span class="keyword">var</span> identity = <span class="keyword">new</span> <span class="type">ClaimsIdentity</span>(claims, Scheme.Name);
<span class="keyword">var</span> principal = <span class="keyword">new</span> <span class="type">ClaimsPrincipal</span>(identity);
<span class="keyword">var</span> ticket = <span class="keyword">new</span> <span class="type">AuthenticationTicket</span>(principal, Scheme.Name);
<span class="keyword">return</span> AuthenticateResult.<span class="method">Success</span>(ticket);
}
</div>
<div class="success-box">
<strong>✅ نکته کلیدی:</strong> <code>validationResult.Scopes</code> مقادیری مانند
<code>read</code>، <code>write</code>، <code>admin</code>، <code>manage</code> دارد که
مستقیماً با Policyهای تعریف‌شده در <code>XAuthorizationHelper</code> مطابقت می‌کند.
</div>
</div>
</div>
<!-- STEP 3 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۳</div>
<div class="step-title">
اطمینان از همخوانی Policyها با Scopeهای API Key
<span class="step-subtitle">فایل: xIdentityHelper/XAuthorizationHelper.cs</span>
</div>
</div>
<div class="step-body">
<p>
در <code>XAuthorizationHelper</code>، Policyهای مربوط به ApiKey تعریف شده‌اند.
این Policyها باید با مقادیر <code>XApiKeyScope</code> در
<code>xIdentityModels/Constants/XApiKeyScope.cs</code> همخوانی داشته باشند.
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIdentityModels/Constants/XApiKeyScope.cs</span>
<span class="keyword">public enum</span> <span class="type">XApiKeyScope</span>
{
[<span class="method">StringValue</span>(<span class="string">"read"</span>)]
Read,
[<span class="method">StringValue</span>(<span class="string">"write"</span>)]
Write,
[<span class="method">StringValue</span>(<span class="string">"admin"</span>)]
Admin,
[<span class="method">StringValue</span>(<span class="string">"manage"</span>)]
Manage
}
</div>
<p>
و در <code>XAuthorizationHelper</code>:
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIdentityHelper/XAuthorizationHelper.cs — Policyهای موجود ApiKey</span>
<span class="comment">// ApiKey Access Policy — دسترسی به هر یک از Scopeها</span>
result.<span class="method">Add</span>(XPolicies.ApiKeyAccess,
<span class="keyword">new</span> <span class="type">AuthorizationPolicyBuilder</span>()
.<span class="method">RequireClaim</span>(
JwtClaimTypes.Scope,
XApiKeyScope.Read.<span class="method">GetStringValue</span>(), <span class="comment">// "read"</span>
XApiKeyScope.Write.<span class="method">GetStringValue</span>(), <span class="comment">// "write"</span>
XApiKeyScope.Manage.<span class="method">GetStringValue</span>() <span class="comment">// "manage"</span>
)
.<span class="method">Build</span>()
);
<span class="comment">// ApiKey Read Access Policy</span>
result.<span class="method">Add</span>(XPolicies.ApiKeyReadAccess,
<span class="keyword">new</span> <span class="type">AuthorizationPolicyBuilder</span>()
.<span class="method">RequireClaim</span>(JwtClaimTypes.Scope, XApiKeyScope.Read.<span class="method">GetStringValue</span>())
.<span class="method">Build</span>()
);
</div>
<div class="info-box">
<strong>💡 نتیجه:</strong> اگر در گام ۲، Scopeهای واقعی از <code>validationResult.Scopes</code>
به Claims اضافه شوند، این Policyها به‌صورت خودکار با موفقیت بررسی می‌شوند و دسترسی
به Endpointهای محافظت‌شده امکان‌پذیر می‌گردد.
</div>
</div>
</div>
<!-- STEP 4 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۴</div>
<div class="step-title">
استفاده صحیح در Controllerها
<span class="step-subtitle">نمونه استفاده صحیح</span>
</div>
</div>
<div class="step-body">
<p>
پس از اصلاحات بالا، برای محافظت از Endpointها با API Key، باید از Policyهای
مربوطه استفاده کنید:
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// نمونه Endpoint محافظت‌شده با API Key</span>
[<span class="type">RequireXPowered</span>]
[<span class="type">HttpPost</span>(<span class="string">"ApiKeys/Validate"</span>)]
[<span class="type">Authorize</span>(Policy = XPolicies.ApiKeyAccess)] <span class="comment">// ← Policy مخصوص API Key</span>
<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">XApiKeyValidationResult</span>&gt;&gt; <span class="method">ValidateApiKey</span>(
[<span class="type">FromBody</span>] <span class="type">XApiKeyValidationRequest</span> request,
<span class="type">CancellationToken</span> cancellationToken = <span class="keyword">default</span>
)
{
<span class="comment">// پیاده‌سازی</span>
}
</div>
<div class="warning-box">
<strong>⚠️ توجه مهم:</strong> از آنجایی که Policyهای ApiKey بر اساس Scope
تعریف شده‌اند، نباید از Policyهای مبتنی بر Role مانند <code>XPolicies.User</code>
یا <code>XPolicies.EnabledUser</code> استفاده کنید. این Policyها انتظار Role Claim دارند
که API Key ندارد.
</div>
</div>
</div>
<!-- STEP 5 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۵</div>
<div class="step-title">
تست و اعتبارسنجی
<span class="step-subtitle">Validation & Testing</span>
</div>
</div>
<div class="step-body">
<p>
پس از اعمال تغییرات، مسیر تست زیر را دنبال کنید:
</p>
<h4>گام ۵.۱ — ساخت یک API Key جدید</h4>
<div class="code-block" data-lang="HTTP">
<span class="comment"># Request: ساخت API Key جدید برای Application</span>
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"]
}
</div>
<p>
پاسخ باید شامل شیء <code>XApiKeyDto</code> باشد. نکته مهم: کلید اصلی (plainKey) فقط
<strong>یک‌بار</strong> در پاسخ اولیه برگردانده می‌شود و باید آن را در جای امن ذخیره کنید.
</p>
<h4>گام ۵.۲ — تست Endpoint با API Key</h4>
<div class="code-block" data-lang="HTTP">
<span class="comment"># Request: تست Endpoint محافظت‌شده با API Key</span>
GET /Account/Test/HiApiKeyAccess
X-Api-Key: xapp_xxxxxxxxxxxxxxxxxxxxxxxx
X-PoweredBy: {xPoweredValue}
</div>
<h4>گام ۵.۳ — بررسی نتیجه</h4>
<table class="styled-table">
<thead>
<tr>
<th>حالت</th>
<th>پاسخ مورد انتظار</th>
<th>تشخیص</th>
</tr>
</thead>
<tbody>
<tr>
<td>API Key معتبر + Scope کافی</td>
<td><code>200 OK</code></td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
<tr>
<td>API Key نامعتبر</td>
<td><code>401 Unauthorized</code> با پیام «ApiKey not found»</td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
<tr>
<td>API Key منقضی‌شده</td>
<td><code>401</code> با پیام «ApiKey is Expired»</td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
<tr>
<td>Scope ناکافی</td>
<td><code>403 Forbidden</code></td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
<tr>
<td>IP غیرمجاز</td>
<td><code>401</code> با پیام «Client IP not Allowed»</td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
<tr>
<td>Rate Limit رد شده</td>
<td><code>401</code> با پیام «ApiKey Rate Limit Reached»</td>
<td><span class="badge badge-pass">✓ صحیح</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- STEP 6 -->
<div class="step-card">
<div class="step-header">
<div class="step-number">۶</div>
<div class="step-title">
به‌روزرسانی Endpoint «Validate API Key»
<span class="step-subtitle">اصلاح کنترلر موجود</span>
</div>
</div>
<div class="step-body">
<p>
در <code>ApplicationsController+Custom.cs</code>، متد <code>ValidateApiKey</code>
فعلی از <code>GetUserInfo()</code> استفاده می‌کند که نیازمند احراز هویت با
<code>Bearer Token</code> است. برای پیاده‌سازی درست، باید این Endpoint
<strong>خودش با API Key احراز هویت شود</strong> (سناریوی self-validation):
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// xIds/Controllers/Applications/ApplicationsController+Custom.cs</span>
<span class="comment">// نسخه اصلاح‌شده — با Policy API Key</span>
[<span class="type">HttpPost</span>(<span class="string">"ApiKeys/Validate"</span>)]
[<span class="type">AllowAnonymous</span>] <span class="comment">// ← چون خودش API Key را در Body می‌فرستد</span>
<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">XApiKeyValidationResult</span>&gt;&gt; <span class="method">ValidateApiKey</span>(
[<span class="type">FromBody</span>] <span class="type">XApiKeyValidationRequest</span> request,
<span class="type">CancellationToken</span> cancellationToken = <span class="keyword">default</span>
)
{
<span class="keyword">try</span>
{
<span class="comment">// اعتبارسنجی ورودی</span>
<span class="keyword">await</span> ValidationProvider
.<span class="method">GroupValidationBuilder</span>()
.<span class="method">AddNotNull</span>(request)
.<span class="method">AddNotEmpty</span>(request.ApiKey)
.<span class="method">ValidateGroupAsync</span>();
<span class="comment">// استخراج ClientIP از Connection واقعی</span>
<span class="keyword">var</span> clientIP = HttpContext.Connection.RemoteIpAddress?.<span class="method">ToString</span>();
<span class="comment">// اعتبارسنجی مستقیم</span>
<span class="keyword">var</span> result = <span class="keyword">await</span> provider.<span class="method">ValidateApiKey</span>(
apiKey: request.ApiKey,
clientIP: clientIP,
requiredScope: request.RequiredScope,
userInfo: <span class="keyword">null</span>, <span class="comment">// ← userInfo دیگر نیاز نیست</span>
cancellationToken: cancellationToken
);
<span class="keyword">return</span> <span class="method">Ok</span>(result.<span class="method">ToDynamicObject</span>());
}
<span class="keyword">catch</span> (<span class="type">Exception</span> ex)
{
<span class="keyword">var</span> result = <span class="method">GetExceptionActionResult</span>(ex);
<span class="keyword">return</span> result;
}
}
</div>
<div class="info-box">
<strong>💡 چرا AllowAnonymous؟</strong> چون این Endpoint خودش نقش «اعتبارسنج API Key» را
دارد و نباید قبل از رسیدن به آن، احراز هویت شود. منطق اعتبارسنجی درون خودش انجام می‌شود.
</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 5: SUMMARY TABLE -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number">۵</div>
<div class="section-title">
<h3>خلاصه تغییرات</h3>
<div class="subtitle">Summary of Changes</div>
</div>
</div>
<div class="section-content">
<table class="styled-table">
<thead>
<tr>
<th>#</th>
<th>فایل</th>
<th>تغییر</th>
<th>اولویت</th>
</tr>
</thead>
<tbody>
<tr>
<td>۱</td>
<td><span class="file-ref">xIds/DI/XDIHelperExtension.cs</span></td>
<td>افزودن <code>ForwardDefaultSelector</code> برای انتخاب پویا بین JwtBearer و XApiKey</td>
<td><span class="badge badge-fail">حیاتی</span></td>
</tr>
<tr>
<td>۲</td>
<td><span class="file-ref">xIds/Providers/XApiKeyAuthenticationHandler.cs</span></td>
<td>حذف Scope ثابت «apikey» و افزودن Scopeهای واقعی از <code>validationResult.Scopes</code></td>
<td><span class="badge badge-fail">حیاتی</span></td>
</tr>
<tr>
<td>۳</td>
<td><span class="file-ref">xIds/Startup.cs</span></td>
<td>اطمینان از ترتیب صحیح ثبت Schemeها (نیاز به تغییر مستقیم ندارد اگر از Extension استفاده شود)</td>
<td><span class="badge badge-warn">متوسط</span></td>
</tr>
<tr>
<td>۴</td>
<td><span class="file-ref">xIds/Controllers/Applications/ApplicationsController+Custom.cs</span></td>
<td>اصلاح <code>ValidateApiKey</code> برای کارکرد مستقل</td>
<td><span class="badge badge-warn">متوسط</span></td>
</tr>
<tr>
<td>۵</td>
<td><span class="file-ref">xIds/Controllers/Account/AccountController+Test.cs</span></td>
<td>افزودن تست‌های احراز هویت با API Key (اختیاری، برای تست)</td>
<td><span class="badge badge-info">توصیه‌شده</span></td>
</tr>
</tbody>
</table>
<div class="success-box">
<strong>✅ نتیجه نهایی:</strong> با اعمال گام‌های ۱ و ۲ (که حیاتی هستند)، مشکل احراز هویت
با <code>XApiKey</code> به‌طور کامل حل می‌شود. گام‌های ۳ تا ۵ برای بهبود و تکمیل توصیه می‌شوند.
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 6: BEST PRACTICES -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number">۶</div>
<div class="section-title">
<h3>بهترین شیوه‌ها و توصیه‌های تکمیلی</h3>
<div class="subtitle">Best Practices & Additional Recommendations</div>
</div>
</div>
<div class="section-content">
<h4>۶.۱ — مدیریت صحیح Scopeها در سطح API Key</h4>
<p>
هنگام ساخت API Key، از مقادیر <code>XApiKeyScope</code> استفاده کنید:
</p>
<div class="code-block" data-lang="C#">
<span class="comment">// نمونه ساخت API Key با Scopeهای صحیح</span>
<span class="keyword">var</span> result = <span class="keyword">await</span> provider.<span class="method">CreateApiKey</span>(
applicationId: applicationId,
scopes: <span class="keyword">new</span>[] { <span class="string">"read"</span>, <span class="string">"write"</span> }, <span class="comment">// ← مطابق XApiKeyScope</span>
allowedIPs: <span class="keyword">new</span>[] { <span class="string">"192.168.1.100"</span> },
rateLimit: 60,
expiration: <span class="type">TimeSpan</span>.<span class="method">FromDays</span>(30),
userInfo: userInfo
);
</div>
<h4>۶.۲ — تفکیک Policyهای مبتنی بر API Key از Policyهای مبتنی بر Role</h4>
<p>
Policyهای موجود در <code>XPolicies</code> دو دسته هستند:
</p>
<ul>
<li><strong>Policyهای مبتنی بر Scope</strong> (مثل <code>ApiKeyAccess</code>, <code>ReadAccess</code>) — برای API Key و Bearer Token</li>
<li><strong>Policyهای مبتنی بر Role</strong> (مثل <code>User</code>, <code>Admin</code>) — فقط برای Bearer Token</li>
</ul>
<div class="warning-box">
<strong>⚠️ توجه:</strong> هرگز از Policyهای Role-based برای Endpointهایی که قرار است با
API Key محافظت شوند استفاده نکنید. این Policyها انتظار Claim از نوع <code>role</code> دارند
که API Key ندارد.
</div>
<h4>۶.۳ — ذخیره امن Plain Key</h4>
<p>
کلید اصلی (plain key) فقط در لحظه ساخت برگردانده می‌شود. باید در جای امن ذخیره شود.
در صورتی که کاربر آن را گم کند، باید کلید قبلی revoke شده و کلید جدید ساخته شود.
</p>
<h4>۶.۴ — Rate Limit و Audit Log</h4>
<p>
در <code>XApiKeyConfiguration</code>، ویژگی‌های زیر فعال هستند:
</p>
<ul>
<li><code>DefaultRateLimit</code> — محدودیت نرخ پیش‌فرض</li>
<li><code>EnableAuditLog</code> — برای ثبت استفاده از API Keyها</li>
</ul>
<p>
توصیه می‌شود در <code>ValidateApiKey</code> پس از اعتبارسنجی موفق، یک رکورد
<code>XApiKeyUsage</code> ثبت کنید تا تاریخچه استفاده قابل ردیابی باشد.
</p>
<h4>۶.۵ — Endpointهای توصیه‌شده برای تست</h4>
<p>
برای اطمینان از کارکرد صحیح، این Endpointها را در <code>AccountController+Test.cs</code>
اضافه کنید:
</p>
<div class="code-block" data-lang="C#">
[<span class="type">RequireXPowered</span>]
[<span class="type">HttpGet</span>(<span class="string">"Test/HiApiKeyReadAccess"</span>)]
[<span class="type">Authorize</span>(Policy = XPolicies.ApiKeyReadAccess)]
<span class="keyword">public</span> <span class="type">ActionResult</span>&lt;<span class="keyword">string</span>&gt; <span class="method">HiApiKeyReadAccess</span>()
{
<span class="keyword">return</span> <span class="method">Ok</span>(<span class="string">"API Key Read Access Passed ..."</span>);
}
[<span class="type">RequireXPowered</span>]
[<span class="type">HttpPost</span>(<span class="string">"Test/HiApiKeyWriteAccess"</span>)]
[<span class="type">Authorize</span>(Policy = XPolicies.ApiKeyWriteAccess)]
<span class="keyword">public</span> <span class="type">ActionResult</span>&lt;<span class="keyword">string</span>&gt; <span class="method">HiApiKeyWriteAccess</span>()
{
<span class="keyword">return</span> <span class="method">Ok</span>(<span class="string">"API Key Write Access Passed ..."</span>);
}
</div>
</div>
</div>
<!-- ══════════════════════════════════════════ -->
<!-- SECTION 7: CONCLUSION -->
<!-- ══════════════════════════════════════════ -->
<div class="section">
<div class="section-header">
<div class="section-number success">۷</div>
<div class="section-title">
<h3>جمع‌بندی</h3>
<div class="subtitle">Conclusion</div>
</div>
</div>
<div class="section-content">
<p>
مشکل عدم احراز هویت با <code>XApiKey</code> در xIds ریشه در <strong>دو نقطه کد کلیدی</strong>
دارد که هر دو در این سند شناسایی و راه‌حل آن‌ها ارائه شد:
</p>
<ol>
<li>
<strong>عدم انتخاب صحیح Scheme در Startup</strong> — که با افزودن
<code>ForwardDefaultSelector</code> در <code>AddJwtBearer</code> حل می‌شود.
</li>
<li>
<strong>عدم تطابق Claims تولیدشده با Policyها</strong> — که با افزودن Scopeهای
واقعی از <code>validationResult.Scopes</code> در <code>XApiKeyAuthenticationHandler</code>
حل می‌شود.
</li>
</ol>
<p>
با اعمال این دو تغییر ساده اما حیاتی، سیستم احراز هویت مبتنی بر API Key به‌طور کامل فعال
می‌شود و Endpointهای محافظت‌شده با Policyهای <code>ApiKeyAccess</code>،
<code>ApiKeyReadAccess</code>، <code>ApiKeyWriteAccess</code> و
<code>ApiKeyAdminAccess</code> به درستی کار می‌کنند.
</p>
<div class="success-box">
<strong>✅ کلید موفقیت:</strong> زیرساخت موجود در xIds برای مدیریت API Key بسیار
کامل و حرفه‌ای است. تنها دو نقطه اتصال کوچک در لایه احراز هویت نیاز به اصلاح داشتند
که در این سند به‌طور کامل تشریح شدند.
</div>
</div>
</div>
<div class="page-info">
این مستند فنی محرمانه بوده و صرفاً جهت استفاده تیم توسعه xSaherelmWorkspace تهیه شده است.
</div>
</div>
<!-- ══════════ FOOTER ══════════ -->
<footer class="document-footer">
<div class="footer-brand">xSaherelmWorkspace — Technical Analysis</div>
<div class="footer-divider"></div>
<div>
تحلیل و مستندسازی: تیم فنی xSaherelmWorkspace &nbsp;|&nbsp; نسخه: ۱.۰ &nbsp;|&nbsp;
مهر ۱۴۰۵
</div>
</footer>
</div>
</body>
</html>