Files
xSaherelmWorkspace/Documents/Docs/Developer/add ApiKey Support 1.html
T
2026-10-04 18:11:59 +03:30

1692 lines
81 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>طراحی قابلیت مدیریت ApiKey و برنامه‌های کاربردی | مستند فنی</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
:root {
--primary: #1a365d;
--primary-light: #2c5282;
--accent: #c9a961;
--accent-dark: #a8893f;
--bg: #faf9f6;
--card-bg: #ffffff;
--text: #1a202c;
--text-muted: #4a5568;
--border: #e2e8f0;
--success: #38a169;
--danger: #e53e3e;
--warning: #dd6b20;
--info: #3182ce;
--purple: #805ad5;
--shadow: 0 4px 6px rgba(0,0,0,0.05), 0 10px 30px rgba(26,54,93,0.08);
}
* { 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: 1050px;
margin: 0 auto;
background: var(--card-bg);
box-shadow: var(--shadow);
border-radius: 12px;
overflow: hidden;
}
.document-header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
padding: 50px 40px 40px;
position: relative;
border-bottom: 4px solid var(--accent);
}
.document-header::before {
content: '';
position: absolute;
top: 0; right: 0;
width: 250px; height: 250px;
background: radial-gradient(circle, rgba(201,169,97,0.15) 0%, transparent 70%);
border-radius: 50%;
transform: translate(50%, -50%);
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
position: relative;
z-index: 1;
}
.company-badge {
display: flex;
align-items: center;
gap: 12px;
}
.company-logo {
width: 60px; height: 60px;
background: var(--accent);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.doc-meta {
text-align: left;
font-size: 12px;
opacity: 0.9;
}
.doc-meta div { margin-bottom: 3px; }
.document-title {
text-align: center;
position: relative;
z-index: 1;
padding: 20px 0;
}
.document-title .label {
display: inline-block;
background: rgba(201,169,97,0.2);
color: var(--accent);
padding: 4px 16px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
margin-bottom: 15px;
border: 1px solid rgba(201,169,97,0.4);
}
.document-title h1 {
font-size: 28px;
font-weight: 800;
margin-bottom: 10px;
letter-spacing: -0.5px;
}
.document-title h2 {
font-size: 16px;
font-weight: 400;
opacity: 0.9;
}
.document-body {
padding: 40px;
}
.intro-text {
background: linear-gradient(to left, #f7fafc, #edf2f7);
border-right: 4px solid var(--accent);
padding: 20px 25px;
border-radius: 8px;
margin-bottom: 35px;
font-size: 14px;
color: var(--text-muted);
}
.section {
margin-bottom: 45px;
page-break-inside: avoid;
}
.section-header {
display: flex;
align-items: center;
gap: 15px;
padding-bottom: 12px;
border-bottom: 2px solid var(--border);
margin-bottom: 20px;
}
.section-number {
width: 50px; height: 50px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 20px;
flex-shrink: 0;
box-shadow: 0 4px 10px rgba(26,54,93,0.2);
}
.section-title { flex: 1; }
.section-title h3 {
font-size: 19px;
font-weight: 700;
color: var(--primary);
margin-bottom: 2px;
}
.section-title .subtitle {
font-size: 12px;
color: var(--text-muted);
}
.section-content { padding-right: 10px; }
.section-content p {
margin-bottom: 12px;
text-align: justify;
text-indent: 20px;
}
.section-content h4 {
color: var(--primary);
font-size: 16px;
margin: 20px 0 12px;
padding-right: 10px;
border-right: 3px solid var(--accent);
}
.section-content ul, .section-content ol {
padding-right: 25px;
margin: 12px 0;
}
.section-content li {
margin-bottom: 8px;
text-align: justify;
}
.section-content li strong { color: var(--primary); }
.highlight-box {
background: linear-gradient(to left, #fffaf0, #fef5e7);
border: 1px solid #f6e05e;
border-right: 4px solid var(--accent);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.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);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.success-box {
background: linear-gradient(to left, #f0fff4, #e6fffa);
border: 1px solid #9ae6b4;
border-right: 4px solid var(--success);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.warning-box {
background: linear-gradient(to left, #fffaf0, #fef5e7);
border: 1px solid #fbd38d;
border-right: 4px solid var(--warning);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.danger-box {
background: linear-gradient(to left, #fff5f5, #fee);
border: 1px solid #feb2b2;
border-right: 4px solid var(--danger);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.purple-box {
background: linear-gradient(to left, #faf5ff, #f3e8ff);
border: 1px solid #d6bcfa;
border-right: 4px solid var(--purple);
padding: 18px 22px;
border-radius: 8px;
margin: 20px 0;
font-size: 14px;
}
.styled-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
font-size: 13px;
}
.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;
}
.styled-table td {
padding: 13px 16px;
border-bottom: 1px solid var(--border);
}
.styled-table tbody tr:last-child td { border-bottom: none; }
.styled-table tbody tr:nth-child(even) { background: #f8fafc; }
.styled-table .highlight-row {
background: #fffaf0 !important;
font-weight: 600;
}
.component-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin: 25px 0;
}
.component-card {
background: #f8fafc;
border: 2px solid var(--border);
border-radius: 10px;
padding: 18px;
transition: all 0.3s;
}
.component-card:hover {
border-color: var(--accent);
box-shadow: 0 4px 12px rgba(201,169,97,0.15);
transform: translateY(-2px);
}
.component-card .card-icon {
width: 40px; height: 40px;
background: linear-gradient(135deg, var(--primary), var(--primary-light));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 800;
font-size: 18px;
margin-bottom: 12px;
}
.component-card h4 {
font-size: 15px;
font-weight: 700;
color: var(--primary);
margin-bottom: 8px;
}
.component-card p {
font-size: 12.5px;
color: var(--text-muted);
text-indent: 0 !important;
margin-bottom: 0 !important;
}
.layer-diagram {
background: linear-gradient(135deg, #f7fafc, #edf2f7);
border: 2px solid var(--border);
border-radius: 12px;
padding: 25px;
margin: 25px 0;
}
.layer-row {
background: white;
border: 2px solid var(--primary-light);
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 15px;
}
.layer-row:last-child { margin-bottom: 0; }
.layer-row .layer-label {
background: var(--primary);
color: white;
padding: 6px 14px;
border-radius: 6px;
font-weight: 700;
font-size: 13px;
min-width: 160px;
text-align: center;
}
.layer-row .layer-content {
flex: 1;
font-size: 13px;
color: var(--text-muted);
}
.layer-row.api { border-color: var(--accent); }
.layer-row.api .layer-label { background: var(--accent); color: var(--primary); }
.layer-row.service { border-color: var(--info); }
.layer-row.service .layer-label { background: var(--info); }
.layer-row.infra { border-color: var(--purple); }
.layer-row.infra .layer-label { background: var(--purple); }
.layer-row.base { border-color: var(--success); }
.layer-row.base .layer-label { background: var(--success); }
.code-block {
background: #1a202c;
color: #e2e8f0;
border-radius: 8px;
padding: 18px 22px;
margin: 15px 0;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12.5px;
line-height: 1.7;
overflow-x: auto;
direction: ltr;
text-align: left;
border-right: 4px solid var(--accent);
}
.code-block .comment { color: #68d391; }
.code-block .keyword { color: #f6ad55; }
.code-block .string { color: #90cdf4; }
.code-block .type { color: #d6bcfa; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin: 25px 0;
}
.stat-card {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
padding: 20px 15px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 12px rgba(26,54,93,0.2);
}
.stat-card .stat-value {
font-size: 28px;
font-weight: 800;
margin-bottom: 5px;
color: var(--accent);
line-height: 1.2;
}
.stat-card .stat-label {
font-size: 11.5px;
opacity: 0.9;
}
.flow-diagram {
background: linear-gradient(135deg, #f7fafc, #edf2f7);
border: 2px solid var(--border);
border-radius: 12px;
padding: 25px;
margin: 25px 0;
}
.flow-step {
background: white;
border: 2px solid var(--primary-light);
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 15px;
position: relative;
}
.flow-step::after {
content: '↓';
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-50%);
font-size: 20px;
color: var(--accent);
font-weight: 800;
}
.flow-step:last-child::after { display: none; }
.flow-step .step-num {
width: 35px; height: 35px;
background: var(--primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 15px;
flex-shrink: 0;
}
.flow-step .step-content {
flex: 1;
font-size: 13.5px;
}
.flow-step .step-content strong {
color: var(--primary);
display: block;
margin-bottom: 3px;
}
.toc {
background: #f8fafc;
border: 1px solid var(--border);
border-radius: 10px;
padding: 25px;
margin: 25px 0;
}
.toc h4 {
color: var(--primary);
margin-bottom: 15px;
font-size: 16px;
border-bottom: 2px solid var(--accent);
padding-bottom: 8px;
}
.toc ol { padding-right: 25px; }
.toc ol li {
margin-bottom: 6px;
font-size: 13.5px;
}
.toc ol li a {
color: var(--primary-light);
text-decoration: none;
border-bottom: 1px dotted var(--primary-light);
}
.conclusion-box {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
padding: 30px;
border-radius: 12px;
margin: 30px 0;
text-align: center;
box-shadow: 0 8px 20px rgba(26,54,93,0.3);
}
.conclusion-box h3 {
font-size: 22px;
font-weight: 800;
margin-bottom: 15px;
color: var(--accent);
}
.conclusion-box p {
font-size: 14px;
line-height: 1.8;
text-indent: 0 !important;
margin-bottom: 15px;
}
.conclusion-box .highlight {
background: rgba(201,169,97,0.2);
padding: 15px 20px;
border-radius: 8px;
margin-top: 20px;
border: 1px solid rgba(201,169,97,0.4);
}
.document-footer {
background: var(--primary);
color: white;
padding: 25px 40px;
text-align: center;
font-size: 12px;
margin-top: 40px;
}
.document-footer .footer-brand {
font-weight: 700;
color: var(--accent);
margin-bottom: 5px;
font-size: 14px;
}
.footer-divider {
width: 60px;
height: 2px;
background: var(--accent);
margin: 10px auto;
}
.page-info {
text-align: center;
padding: 15px;
font-size: 11px;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 30px;
}
@media print {
body { background: white; padding: 0; }
.document-container { box-shadow: none; border-radius: 0; }
.section { page-break-inside: avoid; }
}
@media (max-width: 768px) {
.document-body { padding: 25px 20px; }
.document-header { padding: 30px 20px; }
.component-grid { grid-template-columns: 1fr; }
.stats-grid { grid-template-columns: 1fr 1fr; }
.header-top { flex-direction: column; gap: 15px; }
.doc-meta { text-align: center; }
}
</style>
</head>
<body>
<div class="document-container">
<header class="document-header">
<div class="header-top">
<div class="company-badge">
<div class="company-logo"></div>
</div>
<div class="doc-meta">
<div>شماره مستند: ARCH-APIKEY-1405-002</div>
<div>نسخه: ۱.۰</div>
<div>تاریخ تهیه: ۱۲ مهر ۱۴۰۵</div>
<div>تهیه‌کننده: هادی خزاعی اصل</div>
<div>طبقه‌بندی: محرمانه</div>
</div>
</div>
<div class="document-title">
<span class="label">API KEY MANAGEMENT SYSTEM</span>
<h1>طراحی قابلیت مدیریت ApiKey و برنامه‌های کاربردی</h1>
<h2>بررسی نیازمندی‌ها، معماری و پیاده‌سازی گام به گام</h2>
</div>
</header>
<div class="document-body">
<div class="intro-text">
<strong>توسعه‌دهنده گرامی،</strong><br>
این مستند به‌عنوان یک طرح جامع فنی، قابلیت ایجاد برنامه‌های کاربردی دارای ApiKey را با پیروی از الگوهای OAuth2 در پلتفرم xDashboard بررسی می‌کند. در این سند، علاوه بر نیازمندی‌های اعلام‌شده، نیازمندی‌های تکمیلی و پیشنهادات بهبودی نیز ارائه شده است تا یک راهکار کامل، امن و مقیاس‌پذیر طراحی گردد.
</div>
<!-- فهرست مطالب -->
<div class="toc">
<h4>📑 فهرست مطالب</h4>
<ol>
<li><a href="#s1">خلاصه اجرایی و آمار کلیدی</a></li>
<li><a href="#s2">بررسی نیازمندی‌های اعلام‌شده</a></li>
<li><a href="#s3">نیازمندی‌های تکمیلی و پیشنهادات بهبود</a></li>
<li><a href="#s4">طراحی معماری و مدل داده</a></li>
<li><a href="#s5">مرحله ۱: پیاده‌سازی در xIds</a></li>
<li><a href="#s6">مرحله ۲: سرویس اعتبارسنجی ApiKey</a></li>
<li><a href="#s7">مرحله ۳: سرویس احراز هویت مبتنی بر ApiKey</a></li>
<li><a href="#s8">مرحله ۴: API Endpoints</a></li>
<li><a href="#s9">جریان عملیات و سناریوها</a></li>
<li><a href="#s10">الگوهای امنیتی و ملاحظات</a></li>
<li><a href="#s11">جمع‌بندی و گام‌های بعدی</a></li>
</ol>
</div>
<!-- بخش 1 -->
<div class="section" id="s1">
<div class="section-header">
<div class="section-number">۱</div>
<div class="section-title">
<h3>خلاصه اجرایی و آمار کلیدی</h3>
<div class="subtitle">Executive Summary & Key Statistics</div>
</div>
</div>
<div class="section-content">
<p>سیستم مدیریت ApiKey، یک لایه امنیتی جدید به پلتفرم xDashboard اضافه می‌کند که امکان ایجاد برنامه‌های کاربردی (Applications) با کلیدهای دسترسی موقت را فراهم می‌سازد. این سیستم بر پایه الگوهای OAuth2 Client Credentials طراحی شده و با زیرساخت IdentityServer4 موجود در xIds یکپارچه می‌گردد.</p>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">۳</div>
<div class="stat-label">لایه اصلی پیاده‌سازی</div>
</div>
<div class="stat-card">
<div class="stat-value">۵</div>
<div class="stat-label">Entity جدید</div>
</div>
<div class="stat-card">
<div class="stat-value">۱۲+</div>
<div class="stat-label">نیازمندی تکمیلی</div>
</div>
<div class="stat-card">
<div class="stat-value">۴</div>
<div class="stat-label">مرحله پیاده‌سازی</div>
</div>
</div>
<div class="highlight-box">
<strong>نکته کلیدی:</strong> این راهکار با حفظ سازگاری کامل با الگوهای موجود پروژه (مانند <code>XValidationProvider</code>، <code>XException</code>، <code>XPolicies</code> و <code>IXIdentityManager</code>)، قابلیت ادغام بدون اصطکاک با اکوسیستم فعلی را دارد.
</div>
</div>
</div>
<!-- بخش 2 -->
<div class="section" id="s2">
<div class="section-header">
<div class="section-number">۲</div>
<div class="section-title">
<h3>بررسی نیازمندی‌های اعلام‌شده</h3>
<div class="subtitle">Analysis of Stated Requirements</div>
</div>
</div>
<div class="section-content">
<p>بر اساس درخواست اعلام‌شده، سه نیازمندی اصلی شناسایی شده‌اند:</p>
<table class="styled-table">
<thead>
<tr>
<th>#</th>
<th>نیازمندی</th>
<th>محل پیاده‌سازی</th>
<th>الگوی مرجع</th>
</tr>
</thead>
<tbody>
<tr>
<td>۱</td>
<td>ایجاد برنامه کاربردی و تولید ApiKey با مدت انقضای قابل تنظیم</td>
<td>xIds</td>
<td>OAuth2 Client Credentials</td>
</tr>
<tr>
<td>۲</td>
<td>اعتبارسنجی ApiKey در سرویس‌های مرتبط</td>
<td>xIdentityService</td>
<td>Token Introspection</td>
</tr>
<tr>
<td>۳</td>
<td>تعریف دسترسی بر اساس ApiKey در احراز هویت</td>
<td>xIds + xApi</td>
<td>Policy-based Authorization</td>
</tr>
</tbody>
</table>
<div class="info-box">
<strong>💡 تحلیل اولیه:</strong> این نیازمندی‌ها در واقع پیاده‌سازی یک <strong>OAuth2 Client</strong> کامل هستند که در آن هر "برنامه کاربردی" معادل یک <code>Client</code> در IdentityServer4 بوده و ApiKey معادل <code>ClientSecret</code> است. با این حال، برای مدیریت بهتر چرخه حیات، نیاز به یک لایه انتزاعی بالاتر داریم.
</div>
</div>
</div>
<!-- بخش 3 -->
<div class="section" id="s3">
<div class="section-header">
<div class="section-number">۳</div>
<div class="section-title">
<h3>نیازمندی‌های تکمیلی و پیشنهادات بهبود</h3>
<div class="subtitle">Additional Requirements & Improvement Suggestions</div>
</div>
</div>
<div class="section-content">
<p>در بررسی دقیق‌تر، نیازمندی‌های زیر شناسایی شدند که در درخواست اولیه ذکر نشده‌اند اما برای یک پیاده‌سازی کامل و امن ضروری هستند:</p>
<h4>🔐 الف) نیازمندی‌های امنیتی</h4>
<div class="component-grid">
<div class="component-card">
<div class="card-icon">🔑</div>
<h4>۱. چرخش کلید (Key Rotation)</h4>
<p>امکان تولید کلید جدید بدون اختلال در سرویس‌های در حال اجرا، با پشتیبانی از همزیستی موقت کلید قدیم و جدید.</p>
</div>
<div class="component-card">
<div class="card-icon">🚫</div>
<h4>۲. ابطال فوری (Revocation)</h4>
<p>امکان Revoke کردن ApiKey قبل از انقضای طبیعی، برای موارد اضطراری مانند نشت کلید.</p>
</div>
<div class="component-card">
<div class="card-icon">🌐</div>
<h4>۳. IP Whitelist</h4>
<p>محدودسازی استفاده از ApiKey به IPهای مشخص برای کاهش ریسک سوءاستفاده در صورت نشت.</p>
</div>
<div class="component-card">
<div class="card-icon">🔒</div>
<h4>۴. Rate Limiting</h4>
<p>محدودیت تعداد درخواست در واحد زمان برای هر ApiKey جهت جلوگیری از سوءاستفاده و حملات DoS.</p>
</div>
</div>
<h4>📊 ب) نیازمندی‌های مدیریتی</h4>
<div class="component-grid">
<div class="component-card">
<div class="card-icon">📝</div>
<h4>۵. Metadata و توضیحات</h4>
<p>ذخیره نام برنامه، توضیحات، نام مالک و اطلاعات تماس برای هر ApiKey.</p>
</div>
<div class="component-card">
<div class="card-icon">👤</div>
<h4>۶. Owner Assignment</h4>
<p>تخصیص هر ApiKey به یک کاربر یا سازمان مشخص برای مدیریت متمرکز و حسابرسی.</p>
</div>
<div class="component-card">
<div class="card-icon">📈</div>
<h4>۷. Audit Log</h4>
<p>ثبت کامل لاگ استفاده از هر ApiKey شامل زمان، IP، Endpoint و نتیجه درخواست.</p>
</div>
<div class="component-card">
<div class="card-icon">🏷️</div>
<h4>۸. Scope Management</h4>
<p>تعریف دقیق Scopeهای مجاز برای هر ApiKey (مانند read, write, admin) مشابه OAuth2.</p>
</div>
</div>
<h4>⚙️ ج) نیازمندی‌های عملیاتی</h4>
<div class="component-grid">
<div class="component-card">
<div class="card-icon">🔔</div>
<h4>۹. Notification System</h4>
<p>اطلاع‌رسانی به مالک قبل از انقضای ApiKey (مثلاً ۷ روز قبل) از طریق Email/SMS.</p>
</div>
<div class="component-card">
<div class="card-icon">🔄</div>
<h4>۱۰. Auto-Extension</h4>
<p>امکان تمدید خودکار ApiKey در صورت فعال بودن برنامه (اختیاری).</p>
</div>
<div class="component-card">
<div class="card-icon">📦</div>
<h4>۱۱. Bulk Operations</h4>
<p>امکان ایجاد، ابطال یا تمدید دسته‌ای ApiKeyها برای مدیریت در مقیاس بزرگ.</p>
</div>
<div class="component-card">
<div class="card-icon">🗂️</div>
<h4>۱۲. Tagging & Categorization</h4>
<p>دسته‌بندی ApiKeyها بر اساس نوع برنامه (Mobile, Web, IoT, Third-party) برای گزارش‌گیری.</p>
</div>
</div>
<div class="warning-box">
<strong>⚠️ پیشنهاد مهم:</strong> پیاده‌سازی حداقل موارد <strong>۱ تا ۴</strong> (چرخش کلید، ابطال فوری، IP Whitelist، Rate Limiting) برای یک سیستم تولیدی (Production) <strong>الزامی</strong> است. سایر موارد بر اساس اولویت کسب‌وکار در فازهای بعدی قابل اضافه شدن هستند.
</div>
</div>
</div>
<!-- بخش 4 -->
<div class="section" id="s4">
<div class="section-header">
<div class="section-number">۴</div>
<div class="section-title">
<h3>طراحی معماری و مدل داده</h3>
<div class="subtitle">Architecture Design & Data Model</div>
</div>
</div>
<div class="section-content">
<h4>🏗️ معماری کلی سیستم</h4>
<div class="layer-diagram">
<div class="layer-row api">
<div class="layer-label">لایه API</div>
<div class="layer-content"><strong>ApplicationController</strong> — مدیریت برنامه‌ها و ApiKeyها (CRUD)</div>
</div>
<div class="layer-row service">
<div class="layer-label">لایه سرویس</div>
<div class="layer-content"><strong>XApplicationManager</strong> — منطق تجاری، تولید کلید، مدیریت چرخه حیات</div>
</div>
<div class="layer-row infra">
<div class="layer-label">لایه زیرساخت</div>
<div class="layer-content"><strong>XApiKeyValidator</strong> + <strong>XApiKeyAuthProvider</strong> — اعتبارسنجی و احراز هویت</div>
</div>
<div class="layer-row base">
<div class="layer-label">لایه داده</div>
<div class="layer-content"><strong>XApplication</strong> + <strong>XApiKey</strong> + <strong>XApiKeyUsageLog</strong> — Entityهای جدید</div>
</div>
</div>
<h4>📋 مدل‌های داده پیشنهادی</h4>
<div class="code-block">
<span class="comment">// Entity: XApplication (برنامه کاربردی)</span><br>
<span class="keyword">public class</span> <span class="type">XApplication</span> : <span class="type">XBaseGuidIDEntity</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Required][StringLength(<span class="string">255</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> Name { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringLength(<span class="string">1000</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> Description { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Required][StringLength(<span class="string">255</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> OwnerId { <span class="keyword">get</span>; <span class="keyword">set</span>; } <span class="comment">// XUser.Id</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">XApplicationType</span> Type { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public bool</span> IsActive { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="keyword">true</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span> CreatedOn { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span> UpdatedAt { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Navigation</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public virtual</span> <span class="type">ICollection</span>&lt;<span class="type">XApiKey</span>&gt; ApiKeys { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
}
</div>
<div class="code-block">
<span class="comment">// Entity: XApiKey (کلید دسترسی)</span><br>
<span class="keyword">public class</span> <span class="type">XApiKey</span> : <span class="type">XBaseGuidIDEntity</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Required]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">Guid</span> ApplicationId { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Required][StringLength(<span class="string">512</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> KeyHash { <span class="keyword">get</span>; <span class="keyword">set</span>; } <span class="comment">// SHA256 of ApiKey</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringLength(<span class="string">100</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> KeyPrefix { <span class="keyword">get</span>; <span class="keyword">set</span>; } <span class="comment">// "xapp_abc123..."</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[Required]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span> ExpiresAt { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span> CreatedOn { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span>? LastUsedAt { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span>? RevokedAt { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> RevokedBy { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public bool</span> IsRevoked =&gt; RevokedAt.HasValue;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public bool</span> IsExpired =&gt; <span class="type">DateTime</span>.UtcNow &gt;= ExpiresAt;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public bool</span> IsActive =&gt; !IsRevoked &amp;&amp; !IsExpired;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Scope &amp; Restrictions</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringLength(<span class="string">1000</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> AllowedScopes { <span class="keyword">get</span>; <span class="keyword">set</span>; } <span class="comment">// JSON array</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringLength(<span class="string">2000</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> AllowedIPs { <span class="keyword">get</span>; <span class="keyword">set</span>; } <span class="comment">// Comma-separated</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> RateLimitPerMinute { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">60</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Navigation</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public virtual</span> <span class="type">XApplication</span> Application { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
}
</div>
<div class="code-block">
<span class="comment">// Entity: XApiKeyUsageLog (لاگ استفاده)</span><br>
<span class="keyword">public class</span> <span class="type">XApiKeyUsageLog</span> : <span class="type">XBaseLongIDEntity</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">Guid</span> ApiKeyId { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> Endpoint { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> HttpMethod { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public string</span> ClientIP { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> StatusCode { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DateTime</span> RequestedOn { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public long</span> ResponseTimeMs { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
}
</div>
<h4>🔢 Enumهای مورد نیاز</h4>
<div class="code-block">
<span class="keyword">public enum</span> <span class="type">XApplicationType</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;Web, Mobile, Desktop, IoT, ThirdParty, Service<br>
}<br>
<br>
<span class="keyword">public enum</span> <span class="type">XApiKeyScope</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringValue(<span class="string">"read"</span>)] Read,<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringValue(<span class="string">"write"</span>)] Write,<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringValue(<span class="string">"admin"</span>)] Admin,<br>
&nbsp;&nbsp;&nbsp;&nbsp;[StringValue(<span class="string">"manage"</span>)] Manage<br>
}
</div>
</div>
</div>
<!-- بخش 5 -->
<div class="section" id="s5">
<div class="section-header">
<div class="section-number">۵</div>
<div class="section-title">
<h3>مرحله ۱: پیاده‌سازی در xIds</h3>
<div class="subtitle">Phase 1: Implementation in xIds Module</div>
</div>
</div>
<div class="section-content">
<h4>📌 گام ۱.۱: افزودن تنظیمات قابل پیکربندی</h4>
<p>در کلاس <code>XIdentityConfiguration</code> (موجود در xIdentityModels)، بخش جدید برای تنظیمات ApiKey اضافه می‌شود:</p>
<div class="code-block">
<span class="comment">// File: xIdentityModels/Configurations/XIdentityConfiguration.cs</span><br>
<span class="keyword">public class</span> <span class="type">XIdentityConfiguration</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ... existing properties ...</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">XApiKeyConfiguration</span> ApiKey { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="keyword">new</span>();<br>
}<br>
<br>
<span class="keyword">public class</span> <span class="type">XApiKeyConfiguration</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Default expiration in minutes (e.g., 43200 = 30 days)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> DefaultExpirationMinutes { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">43200</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Maximum allowed expiration (e.g., 525600 = 1 year)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> MaxExpirationMinutes { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">525600</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Maximum ApiKeys per Application</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> MaxKeysPerApplication { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">10</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Notification before expiration (minutes)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> NotifyBeforeMinutes { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">10080</span>; <span class="comment">// 7 days</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Enable audit logging</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public bool</span> EnableAuditLog { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="keyword">true</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Default rate limit (requests per minute)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public int</span> DefaultRateLimit { <span class="keyword">get</span>; <span class="keyword">set</span>; } = <span class="string">60</span>;<br>
}
</div>
<h4>📌 گام ۱.۲: افزودن DbContext و Migration</h4>
<p>در <code>XIdentityDbContext</code>، DbSetهای جدید اضافه می‌شوند:</p>
<div class="code-block">
<span class="comment">// File: xIds/DbContext/XIdentityDbContext.cs</span><br>
<span class="keyword">public class</span> <span class="type">XIdentityDbContext</span> : <span class="type">IdentityDbContext</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ... existing DbSets ...</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DbSet</span>&lt;<span class="type">XApplication</span>&gt; Applications { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DbSet</span>&lt;<span class="type">XApiKey</span>&gt; ApiKeys { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public</span> <span class="type">DbSet</span>&lt;<span class="type">XApiKeyUsageLog</span>&gt; ApiKeyUsageLogs { <span class="keyword">get</span>; <span class="keyword">set</span>; }<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">protected override void</span> <span class="type">OnModelCreating</span>(<span class="type">ModelBuilder</span> modelBuilder)<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">base</span>.OnModelCreating(modelBuilder);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Configure XApplication</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;modelBuilder.Entity&lt;<span class="type">XApplication</span>&gt;(e =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.HasIndex(a =&gt; a.Name).IsUnique();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.HasMany(a =&gt; a.ApiKeys)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.WithOne(k =&gt; k.Application)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.HasForeignKey(k =&gt; k.ApplicationId);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Configure XApiKey</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;modelBuilder.Entity&lt;<span class="type">XApiKey</span>&gt;(e =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.HasIndex(k =&gt; k.KeyHash).IsUnique();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;e.HasIndex(k =&gt; k.ExpiresAt);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;});<br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
}
</div>
<h4>📌 گام ۱.۳: پیاده‌سازی XApplicationManager</h4>
<p>یک کلاس جدید برای مدیریت برنامه‌ها و ApiKeyها مشابه الگوی <code>XIdentityManager</code> موجود:</p>
<div class="code-block">
<span class="comment">// File: xIds/Interfaces/IXApplicationManager.cs</span><br>
<span class="keyword">public interface</span> <span class="type">IXApplicationManager</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Application CRUD</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApplicationDto</span>&gt; CreateApplication(<span class="type">XApplicationDto</span> item, <span class="keyword">string</span> ownerId);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApplicationDto</span>&gt; UpdateApplication(<span class="type">Guid</span> id, <span class="type">XApplicationDto</span> item);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="keyword">bool</span>&gt; DeleteApplication(<span class="type">Guid</span> id);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApplicationDto</span>&gt; GetApplication(<span class="type">Guid</span> id);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">IEnumerable</span>&lt;<span class="type">XApplicationDto</span>&gt;&gt; GetOwnerApplications(<span class="keyword">string</span> ownerId);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ApiKey Management</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApiKeyCreationResult</span>&gt; CreateApiKey(<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Guid</span> applicationId,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">TimeSpan</span>? expiration = <span class="keyword">null</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">IEnumerable</span>&lt;<span class="keyword">string</span>&gt; scopes = <span class="keyword">null</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">IEnumerable</span>&lt;<span class="keyword">string</span>&gt; allowedIPs = <span class="keyword">null</span>,<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">int</span>? rateLimit = <span class="keyword">null</span>);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="keyword">bool</span>&gt; RevokeApiKey(<span class="type">Guid</span> apiKeyId, <span class="keyword">string</span> revokedBy);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApiKeyDto</span>&gt; RotateApiKey(<span class="type">Guid</span> apiKeyId, <span class="type">TimeSpan</span>? newExpiration = <span class="keyword">null</span>);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">IEnumerable</span>&lt;<span class="type">XApiKeyDto</span>&gt;&gt; GetApplicationApiKeys(<span class="type">Guid</span> applicationId);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Validation</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Task</span>&lt;<span class="type">XApiKeyValidationResult</span>&gt; ValidateApiKey(<span class="keyword">string</span> apiKey, <span class="keyword">string</span> clientIP);<br>
}
</div>
<h4>📌 گام ۱.۴: تولید امن ApiKey</h4>
<p>الگوی تولید کلید باید از نظر رمزنگاری امن باشد:</p>
<div class="code-block">
<span class="comment">// File: xIds/Helpers/XApiKeyGenerator.cs</span><br>
<span class="keyword">public static class</span> <span class="type">XApiKeyGenerator</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public static</span> (<span class="keyword">string</span> plainKey, <span class="keyword">string</span> hash, <span class="keyword">string</span> prefix) <span class="type">Generate</span>()<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Generate 32 bytes of cryptographically secure random data</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> randomBytes = <span class="keyword">new byte</span>[<span class="string">32</span>];<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">using</span> (<span class="keyword">var</span> rng = <span class="type">RandomNumberGenerator</span>.Create())<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rng.GetBytes(randomBytes);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Format: xapp_{base64url}</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> plainKey = <span class="string">$"xapp_{Convert.ToBase64String(randomBytes)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="string">.Replace("+", "-").Replace("/", "_").TrimEnd('=')}"</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Hash with SHA256 for storage (never store plain key)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">using</span> (<span class="keyword">var</span> sha256 = <span class="type">SHA256</span>.Create())<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> hashBytes = sha256.ComputeHash(<span class="type">Encoding</span>.UTF8.GetBytes(plainKey));<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> hash = <span class="type">BitConverter</span>.ToString(hashBytes).Replace(<span class="string">"-"</span>, <span class="string">""</span>).ToLower();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Prefix for quick identification (first 12 chars)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> prefix = plainKey.Substring(<span class="string">0</span>, <span class="string">16</span>) + <span class="string">"..."</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> (plainKey, hash, prefix);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public static bool</span> <span class="type">Verify</span>(<span class="keyword">string</span> plainKey, <span class="keyword">string</span> storedHash)<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">using</span> (<span class="keyword">var</span> sha256 = <span class="type">SHA256</span>.Create())<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> hashBytes = sha256.ComputeHash(<span class="type">Encoding</span>.UTF8.GetBytes(plainKey));<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> computedHash = <span class="type">BitConverter</span>.ToString(hashBytes)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.Replace(<span class="string">"-"</span>, <span class="string">""</span>).ToLower();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> computedHash == storedHash;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
}
</div>
<div class="success-box">
<strong>✅ ویژگی‌های امنیتی این طراحی:</strong>
<ul style="padding-right: 20px; margin-top: 10px;">
<li>کلید ApiKey هرگز به‌صورت Plain Text ذخیره نمی‌شود (فقط Hash)</li>
<li>استفاده از <code>RandomNumberGenerator</code> برای تولید امن</li>
<li>پیشوند <code>xapp_</code> برای شناسایی سریع نوع کلید</li>
<li>Prefix کوتاه برای نمایش در UI بدون افشای کلید کامل</li>
</ul>
</div>
</div>
</div>
<!-- بخش 6 -->
<div class="section" id="s6">
<div class="section-header">
<div class="section-number">۶</div>
<div class="section-title">
<h3>مرحله ۲: سرویس اعتبارسنجی ApiKey</h3>
<div class="subtitle">Phase 2: ApiKey Validation Service</div>
</div>
</div>
<div class="section-content">
<h4>📌 گام ۲.۱: طراحی XApiKeyValidator</h4>
<p>این سرویس مسئولیت اعتبارسنجی کامل ApiKey را بر عهده دارد:</p>
<div class="code-block">
<span class="comment">// File: xIds/Validators/XApiKeyValidator.cs</span><br>
<span class="keyword">public class</span> <span class="type">XApiKeyValidator</span> : <span class="type">IXApiKeyValidator</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">private readonly</span> <span class="type">XIdentityDbContext</span> _dbContext;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">private readonly</span> <span class="type">IXIdentityConfiguration</span> _config;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">private readonly</span> <span class="type">IMemoryCache</span> _cache; <span class="comment">// For rate limiting</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">XApiKeyValidationResult</span>&gt; <span class="type">ValidateAsync</span>(<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> apiKey, <span class="keyword">string</span> clientIP, <span class="keyword">string</span> requiredScope = <span class="keyword">null</span>)<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> result = <span class="keyword">new</span> <span class="type">XApiKeyValidationResult</span>();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 1: Compute hash of provided key</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> hash = <span class="type">XApiKeyGenerator</span>.ComputeHash(apiKey);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 2: Lookup in database</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> keyEntity = <span class="keyword">await</span> _dbContext.ApiKeys<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.Include(k =&gt; k.Application)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.FirstOrDefaultAsync(k =&gt; k.KeyHash == hash);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (keyEntity == <span class="keyword">null</span>)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.InvalidApiKey;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 3: Check expiration</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (keyEntity.IsExpired)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.ApiKeyExpired;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 4: Check revocation</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (keyEntity.IsRevoked)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.ApiKeyRevoked;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 5: Check application active</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!keyEntity.Application.IsActive)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.ApplicationDisabled;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 6: Check IP whitelist</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!<span class="keyword">string</span>.IsNullOrEmpty(keyEntity.AllowedIPs))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> allowedIPs = keyEntity.AllowedIPs.Split(<span class="string">','</span>);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!allowedIPs.Contains(clientIP))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.IPNotAllowed;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 7: Check rate limit</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!<span class="type">CheckRateLimit</span>(keyEntity.Id, keyEntity.RateLimitPerMinute))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.RateLimitExceeded;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 8: Check scope (if required)</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!<span class="keyword">string</span>.IsNullOrEmpty(requiredScope))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> scopes = <span class="type">JsonConvert</span>.DeserializeObject&lt;<span class="type">List</span>&lt;<span class="keyword">string</span>&gt;&gt;(keyEntity.AllowedScopes);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!scopes.Contains(requiredScope))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Error = <span class="type">XException</span>.InsufficientScope;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 9: Update LastUsedAt</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;keyEntity.LastUsedAt = <span class="type">DateTime</span>.UtcNow;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">await</span> _dbContext.SaveChangesAsync();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Step 10: Build success result</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.IsValid = <span class="keyword">true</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.ApplicationId = keyEntity.ApplicationId;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.OwnerId = keyEntity.Application.OwnerId;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;result.Scopes = <span class="type">JsonConvert</span>.DeserializeObject&lt;<span class="type">List</span>&lt;<span class="keyword">string</span>&gt;&gt;(keyEntity.AllowedScopes);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> result;<br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
}
</div>
<h4>📌 گام ۲.۲: پیاده‌سازی Rate Limiter</h4>
<div class="code-block">
<span class="keyword">private bool</span> <span class="type">CheckRateLimit</span>(<span class="type">Guid</span> apiKeyId, <span class="keyword">int</span> limitPerMinute)<br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> cacheKey = <span class="string">$"ratelimit:{apiKeyId}"</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> currentCount = _cache.GetOrCreate(cacheKey, entry =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;entry.SlidingExpiration = <span class="type">TimeSpan</span>.FromMinutes(<span class="string">1</span>);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="string">0</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;});<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (currentCount &gt;= limitPerMinute)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return false</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;_cache.Set(cacheKey, currentCount + <span class="string">1</span>, <span class="type">TimeSpan</span>.FromMinutes(<span class="string">1</span>));<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return true</span>;<br>
}
</div>
</div>
</div>
<!-- بخش 7 -->
<div class="section" id="s7">
<div class="section-header">
<div class="section-number">۷</div>
<div class="section-title">
<h3>مرحله ۳: سرویس احراز هویت مبتنی بر ApiKey</h3>
<div class="subtitle">Phase 3: ApiKey Authentication Service</div>
</div>
</div>
<div class="section-content">
<h4>📌 گام ۳.۱: ایجاد Authentication Handler</h4>
<p>برای یکپارچگی با ASP.NET Core Authentication، یک Handler جدید ایجاد می‌کنیم:</p>
<div class="code-block">
<span class="comment">// File: xIdentityService/Authentication/XApiKeyAuthenticationHandler.cs</span><br>
<span class="keyword">public class</span> <span class="type">XApiKeyAuthenticationHandler</span> : <span class="type">AuthenticationHandler</span>&lt;<span class="type">XApiKeyAuthenticationOptions</span>&gt;<br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public const string</span> AuthenticationScheme = <span class="string">"XApiKey"</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public const string</span> HeaderName = <span class="string">"X-Api-Key"</span>;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">private readonly</span> <span class="type">IXApiKeyValidator</span> _validator;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">protected override async</span> <span class="type">Task</span>&lt;<span class="type">AuthenticateResult</span>&gt; <span class="type">HandleAuthenticateAsync</span>()<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Extract ApiKey from header</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!Request.Headers.ContainsKey(HeaderName))<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="type">AuthenticateResult</span>.NoResult();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> apiKey = Request.Headers[HeaderName].ToString();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> clientIP = Request.HttpContext.Connection.RemoteIpAddress?.ToString();<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Validate ApiKey</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> validationResult = <span class="keyword">await</span> _validator.ValidateAsync(apiKey, clientIP);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!validationResult.IsValid)<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="type">AuthenticateResult</span>.Fail(validationResult.Error.Message);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Build ClaimsPrincipal</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> claims = <span class="keyword">new</span>[]<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">new</span> <span class="type">Claim</span>(<span class="type">ClaimTypes</span>.Name, validationResult.OwnerId),<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"application_id"</span>, validationResult.ApplicationId.ToString()),<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">new</span> <span class="type">Claim</span>(<span class="string">"auth_type"</span>, <span class="string">"apikey"</span>),<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">new</span> <span class="type">Claim</span>(<span class="type">JwtClaimTypes</span>.Scope, <span class="string">"apikey"</span>),<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Add scope claims</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> scopeClaims = validationResult.Scopes<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.Select(s =&gt; <span class="keyword">new</span> <span class="type">Claim</span>(<span class="type">JwtClaimTypes</span>.Scope, s));<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> identity = <span class="keyword">new</span> <span class="type">ClaimsIdentity</span>(claims.Union(scopeClaims), AuthenticationScheme);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> principal = <span class="keyword">new</span> <span class="type">ClaimsPrincipal</span>(identity);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> ticket = <span class="keyword">new</span> <span class="type">AuthenticationTicket</span>(principal, AuthenticationScheme);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">return</span> <span class="type">AuthenticateResult</span>.Success(ticket);<br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
}
</div>
<h4>📌 گام ۳.۲: ثبت در Startup</h4>
<div class="code-block">
<span class="comment">// File: xApi/Startup.cs - ConfigureServices</span><br>
services.AddAuthentication(options =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;options.DefaultScheme = <span class="type">JwtBearerDefaults</span>.AuthenticationScheme;<br>
})<br>
.AddJwtBearer(<span class="comment">/* existing config */</span>)<br>
.AddScheme&lt;<span class="type">XApiKeyAuthenticationOptions</span>, <span class="type">XApiKeyAuthenticationHandler</span>&gt;(<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">XApiKeyAuthenticationHandler</span>.AuthenticationScheme,<br>
&nbsp;&nbsp;&nbsp;&nbsp;options =&gt; { });<br>
<br>
<span class="comment">// Authorization policies for ApiKey</span><br>
services.AddAuthorization(options =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;options.AddPolicy(<span class="string">"ApiKeyAccess"</span>, policy =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;policy.AddAuthenticationSchemes(<span class="type">XApiKeyAuthenticationHandler</span>.AuthenticationScheme);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;policy.RequireAuthenticatedUser();<br>
&nbsp;&nbsp;&nbsp;&nbsp;});<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Scope-based policies</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;options.AddPolicy(<span class="string">"ApiKeyRead"</span>, policy =&gt; {<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;policy.AddAuthenticationSchemes(<span class="type">XApiKeyAuthenticationHandler</span>.AuthenticationScheme);<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;policy.RequireClaim(<span class="type">JwtClaimTypes</span>.Scope, <span class="string">"read"</span>, <span class="string">"write"</span>, <span class="string">"admin"</span>);<br>
&nbsp;&nbsp;&nbsp;&nbsp;});<br>
});
</div>
<h4>📌 گام ۳.۳: استفاده در Controllerها</h4>
<div class="code-block">
<span class="comment">// Example: Supporting both JWT and ApiKey authentication</span><br>
[Authorize(Policy = <span class="string">"ApiKeyRead"</span>)]<br>
<span class="keyword">public class</span> <span class="type">DataController</span> : <span class="type">ControllerBase</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpGet(<span class="string">"items"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&gt; GetItems()<br>
&nbsp;&nbsp;&nbsp;&nbsp;{<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Works with both JWT Bearer and X-Api-Key header</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">var</span> userId = User.Identity.Name;<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ...</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;}<br>
}
</div>
</div>
</div>
<!-- بخش 8 -->
<div class="section" id="s8">
<div class="section-header">
<div class="section-number">۸</div>
<div class="section-title">
<h3>مرحله ۴: API Endpoints</h3>
<div class="subtitle">Phase 4: API Endpoints Design</div>
</div>
</div>
<div class="section-content">
<h4>📌 طراحی Controller</h4>
<div class="code-block">
<span class="comment">// File: xIds/Controllers/ApplicationController.cs</span><br>
[ApiController]<br>
[Route(<span class="string">"api/v1/applications"</span>)]<br>
[Authorize(Policy = XPolicies.EnabledUser)]<br>
<span class="keyword">public class</span> <span class="type">ApplicationController</span> : <span class="type">XIBaseController</span><br>
{<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// Application CRUD</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpPost]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">XApplicationDto</span>&gt;&gt; Create(<span class="type">XApplicationDto</span> model);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpGet]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">IEnumerable</span>&lt;<span class="type">XApplicationDto</span>&gt;&gt;&gt; GetMyApplications();<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpPut(<span class="string">"{id}"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&gt; Update(<span class="type">Guid</span> id, <span class="type">XApplicationDto</span> model);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpDelete(<span class="string">"{id}"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&gt; Delete(<span class="type">Guid</span> id);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// ApiKey Management</span><br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpPost(<span class="string">"{appId}/apikeys"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">XApiKeyCreationResponse</span>&gt;&gt; CreateApiKey(<br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="type">Guid</span> appId, <span class="type">XCreateApiKeyRequest</span> request);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpGet(<span class="string">"{appId}/apikeys"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">IEnumerable</span>&lt;<span class="type">XApiKeyDto</span>&gt;&gt;&gt; GetApiKeys(<span class="type">Guid</span> appId);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpPost(<span class="string">"apikeys/{id}/revoke"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&gt; RevokeApiKey(<span class="type">Guid</span> id);<br>
&nbsp;&nbsp;&nbsp;&nbsp;<br>
&nbsp;&nbsp;&nbsp;&nbsp;[HttpPost(<span class="string">"apikeys/{id}/rotate"</span>)]<br>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">public async</span> <span class="type">Task</span>&lt;<span class="type">ActionResult</span>&lt;<span class="type">XApiKeyCreationResponse</span>&gt;&gt; RotateApiKey(<span class="type">Guid</span> id);<br>
}
</div>
<h4>📋 DTOهای مورد نیاز</h4>
<table class="styled-table">
<thead>
<tr>
<th>DTO</th>
<th>کاربرد</th>
<th>فیلدهای کلیدی</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>XApplicationDto</code></td>
<td>نمایش برنامه</td>
<td>Id, Name, Description, Type, IsActive, CreatedOn</td>
</tr>
<tr>
<td><code>XCreateApplicationRequest</code></td>
<td>درخواست ساخت برنامه</td>
<td>Name, Description, Type</td>
</tr>
<tr>
<td><code>XApiKeyDto</code></td>
<td>نمایش ApiKey (بدون کلید کامل)</td>
<td>Id, KeyPrefix, ExpiresAt, LastUsedAt, Scopes, IsActive</td>
</tr>
<tr>
<td><code>XCreateApiKeyRequest</code></td>
<td>درخواست ساخت ApiKey</td>
<td>ExpirationMinutes, Scopes, AllowedIPs, RateLimit</td>
</tr>
<tr>
<td><code>XApiKeyCreationResponse</code></td>
<td>پاسخ ساخت (فقط یکبار کلید کامل)</td>
<td>ApiKey (plain), ExpiresAt, KeyPrefix</td>
</tr>
</tbody>
</table>
<div class="danger-box">
<strong>🔴 نکته امنیتی بسیار مهم:</strong> کلید ApiKey به‌صورت Plain Text <strong>فقط یکبار</strong> در پاسخ <code>XApiKeyCreationResponse</code> به کاربر نمایش داده می‌شود. پس از آن، فقط <code>KeyPrefix</code> قابل مشاهده است و کاربر در صورت فراموشی کلید، باید آن را Rotate کند.
</div>
</div>
</div>
<!-- بخش 9 -->
<div class="section" id="s9">
<div class="section-header">
<div class="section-number">۹</div>
<div class="section-title">
<h3>جریان عملیات و سناریوها</h3>
<div class="subtitle">Operational Flows & Scenarios</div>
</div>
</div>
<div class="section-content">
<h4>🔄 سناریوی ۱: ایجاد برنامه و دریافت ApiKey</h4>
<div class="flow-diagram">
<div class="flow-step">
<div class="step-num">۱</div>
<div class="step-content">
<strong>کاربر درخواست ایجاد برنامه را ارسال می‌کند</strong>
POST /api/v1/applications با payload شامل نام، توضیحات و نوع برنامه
</div>
</div>
<div class="flow-step">
<div class="step-num">۲</div>
<div class="step-content">
<strong>سیستم برنامه را ایجاد می‌کند</strong>
ذخیره XApplication با OwnerId = کاربر فعلی
</div>
</div>
<div class="flow-step">
<div class="step-num">۳</div>
<div class="step-content">
<strong>کاربر درخواست ApiKey می‌کند</strong>
POST /api/v1/applications/{appId}/apikeys با تنظیمات انقضا و scope
</div>
</div>
<div class="flow-step">
<div class="step-num">۴</div>
<div class="step-content">
<strong>سیستم ApiKey تولید و برمی‌گرداند</strong>
کلید Plain Text فقط یکبار نمایش داده می‌شود، Hash در DB ذخیره می‌شود
</div>
</div>
</div>
<h4>🔄 سناریوی ۲: استفاده از ApiKey در درخواست API</h4>
<div class="flow-diagram">
<div class="flow-step">
<div class="step-num">۱</div>
<div class="step-content">
<strong>کلاینت درخواست API را ارسال می‌کند</strong>
Header: <code>X-Api-Key: xapp_abc123...</code>
</div>
</div>
<div class="flow-step">
<div class="step-num">۲</div>
<div class="step-content">
<strong>Authentication Handler کلید را استخراج می‌کند</strong>
XApiKeyAuthenticationHandler فعال می‌شود
</div>
</div>
<div class="flow-step">
<div class="step-num">۳</div>
<div class="step-content">
<strong>اعتبارسنجی کامل انجام می‌شود</strong>
بررسی انقضا، ابطال، IP whitelist، rate limit، scope
</div>
</div>
<div class="flow-step">
<div class="step-num">۴</div>
<div class="step-content">
<strong>ClaimsPrincipal ساخته می‌شود</strong>
با OwnerId، ApplicationId و Scopes
</div>
</div>
<div class="flow-step">
<div class="step-num">۵</div>
<div class="step-content">
<strong>درخواست به Controller هدایت می‌شود</strong>
Authorization policy بررسی و پاسخ ارسال می‌گردد
</div>
</div>
</div>
<h4>🔄 سناریوی ۳: چرخش کلید (Key Rotation)</h4>
<div class="flow-diagram">
<div class="flow-step">
<div class="step-num">۱</div>
<div class="step-content">
<strong>کاربر درخواست چرخش کلید را می‌دهد</strong>
POST /api/v1/applications/apikeys/{id}/rotate
</div>
</div>
<div class="flow-step">
<div class="step-num">۲</div>
<div class="step-content">
<strong>کلید جدید تولید می‌شود</strong>
کلید قدیمی برای مدت grace period (مثلاً ۱ ساعت) فعال می‌ماند
</div>
</div>
<div class="flow-step">
<div class="step-num">۳</div>
<div class="step-content">
<strong>کلید جدید به کاربر داده می‌شود</strong>
کاربر کلید جدید را در برنامه خود جایگزین می‌کند
</div>
</div>
<div class="flow-step">
<div class="step-num">۴</div>
<div class="step-content">
<strong>پس از grace period، کلید قدیمی منقضی می‌شود</strong>
Background job کلیدهای قدیمی را غیرفعال می‌کند
</div>
</div>
</div>
</div>
</div>
<!-- بخش 10 -->
<div class="section" id="s10">
<div class="section-header">
<div class="section-number">۱۰</div>
<div class="section-title">
<h3>الگوهای امنیتی و ملاحظات</h3>
<div class="subtitle">Security Patterns & Considerations</div>
</div>
</div>
<div class="section-content">
<table class="styled-table">
<thead>
<tr>
<th>الگوی امنیتی</th>
<th>پیاده‌سازی</th>
<th>اهمیت</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Hashing کلید</strong></td>
<td>SHA256 - کلید Plain Text هرگز ذخیره نمی‌شود</td>
<td style="color: var(--danger); font-weight: 700;">حیاتی</td>
</tr>
<tr>
<td><strong>HTTPS Only</strong></td>
<td>ApiKey فقط از طریق HTTPS قابل ارسال است</td>
<td style="color: var(--danger); font-weight: 700;">حیاتی</td>
</tr>
<tr>
<td><strong>Rate Limiting</strong></td>
<td>بر اساس ApiKey و IP با MemoryCache</td>
<td style="color: var(--warning); font-weight: 700;">بالا</td>
</tr>
<tr>
<td><strong>IP Whitelist</strong></td>
<td>محدودسازی به IPهای مجاز</td>
<td style="color: var(--warning); font-weight: 700;">بالا</td>
</tr>
<tr>
<td><strong>Scope Management</strong></td>
<td>هر ApiKey scope مشخصی دارد</td>
<td style="color: var(--warning); font-weight: 700;">بالا</td>
</tr>
<tr>
<td><strong>Audit Logging</strong></td>
<td>ثبت تمام استفاده‌ها در XApiKeyUsageLog</td>
<td style="color: var(--info); font-weight: 700;">متوسط</td>
</tr>
<tr>
<td><strong>Expiration</strong></td>
<td>مدت زمان محدود با قابلیت تمدید</td>
<td style="color: var(--warning); font-weight: 700;">بالا</td>
</tr>
<tr>
<td><strong>Revocation</strong></td>
<td>امکان ابطال فوری در صورت نشت</td>
<td style="color: var(--danger); font-weight: 700;">حیاتی</td>
</tr>
<tr>
<td><strong>Key Rotation</strong></td>
<td>چرخش بدون downtime با grace period</td>
<td style="color: var(--info); font-weight: 700;">متوسط</td>
</tr>
<tr>
<td><strong>One-time Display</strong></td>
<td>کلید فقط یکبار به کاربر نمایش داده می‌شود</td>
<td style="color: var(--warning); font-weight: 700;">بالا</td>
</tr>
</tbody>
</table>
<div class="purple-box">
<strong>🎯 یکپارچگی با OAuth2:</strong> این طراحی با الگوی <strong>OAuth2 Client Credentials Grant</strong> همخوانی کامل دارد. در واقع هر Application معادل یک OAuth2 Client و هر ApiKey معادل یک Client Secret است. در آینده می‌توان به‌راحتی این سیستم را به IdentityServer4 متصل کرد و از token endpoint آن برای تبدیل ApiKey به JWT استفاده نمود.
</div>
<h4>🔐 ملاحظات امنیتی تکمیلی</h4>
<ul>
<li><strong>ذخیره‌سازی:</strong> کلید ApiKey هرگز در Log یا Response بعد از ساخت ذخیره نمی‌شود</li>
<li><strong>انتقال:</strong> فقط از طریق Header (<code>X-Api-Key</code>)، نه URL Query</li>
<li><strong>طول کلید:</strong> حداقل ۲۵۶ بیت (۳۲ بایت) entropy</li>
<li><strong>Brute Force Protection:</strong> پس از ۵ تلاش ناموفق، IP برای ۱۵ دقیقه block می‌شود</li>
<li><strong>CORS:</strong> برای Endpoints حساس، CORS باید به‌دقت پیکربندی شود</li>
</ul>
</div>
</div>
<!-- بخش 11 -->
<div class="section" id="s11">
<div class="section-header">
<div class="section-number">۱۱</div>
<div class="section-title">
<h3>جمع‌بندی و گام‌های بعدی</h3>
<div class="subtitle">Summary & Next Steps</div>
</div>
</div>
<div class="section-content">
<h4>✅ خلاصه طراحی</h4>
<p>این مستند یک راهکار کامل برای مدیریت ApiKey در پلتفرم xDashboard ارائه می‌دهد که شامل موارد زیر است:</p>
<ul>
<li>مدل داده کامل با ۳ Entity جدید (XApplication, XApiKey, XApiKeyUsageLog)</li>
<li>پیاده‌سازی امن تولید و ذخیره‌سازی کلید با SHA256</li>
<li>Authentication Handler یکپارچه با ASP.NET Core</li>
<li>سیستم Rate Limiting و IP Whitelist</li>
<li>امکان چرخش کلید بدون downtime</li>
<li>سیستم Audit Log کامل</li>
<li>یکپارچگی با OAuth2 برای توسعه آینده</li>
</ul>
<h4>📋 گام‌های اجرایی پیشنهادی</h4>
<table class="styled-table">
<thead>
<tr>
<th>فاز</th>
<th>فعالیت</th>
<th>زمان تخمینی</th>
</tr>
</thead>
<tbody>
<tr>
<td>۱</td>
<td>طراحی و تصویب مدل داده + Migration</td>
<td>۱ روز</td>
</tr>
<tr>
<td>۲</td>
<td>پیاده‌سازی XApplicationManager + XApiKeyGenerator</td>
<td>۲ روز</td>
</tr>
<tr>
<td>۳</td>
<td>پیاده‌سازی XApiKeyValidator + Rate Limiter</td>
<td>۱ روز</td>
</tr>
<tr>
<td>۴</td>
<td>پیاده‌سازی Authentication Handler</td>
<td>۱ روز</td>
</tr>
<tr>
<td>۵</td>
<td>طراحی و پیاده‌سازی API Endpoints</td>
<td>۲ روز</td>
</tr>
<tr>
<td>۶</td>
<td>تست امنیتی و Unit Test</td>
<td>۲ روز</td>
</tr>
<tr>
<td>۷</td>
<td>مستندسازی API و راهنمای کلاینت</td>
<td>۱ روز</td>
</tr>
<tr class="highlight-row">
<td colspan="2"><strong>مجموع زمان تخمینی</strong></td>
<td><strong>۱۰ روز کاری</strong></td>
</tr>
</tbody>
</table>
<div class="conclusion-box">
<h3>🎯 توصیه نهایی</h3>
<p>این طراحی با رعایت کامل اصول امنیتی و انطباق با الگوهای موجود پروژه (IdentityServer4, XPolicies, XValidationProvider) آماده پیاده‌سازی است. پیشنهاد می‌شود فاز اول با حداقل نیازمندی‌های امنیتی (Hashing, HTTPS, Expiration, Revocation) شروع شود و قابلیت‌های پیشرفته (IP Whitelist, Rate Limiting, Audit Log) در فازهای بعدی اضافه گردند.</p>
<div class="highlight">
<strong>💎 ارزش پیشنهادی:</strong><br>
امنیت سطح Enterprise + یکپارچگی کامل با OAuth2 + مقیاس‌پذیری بالا + قابلیت ردیابی و حسابرسی کامل
</div>
</div>
<div class="info-box">
<strong>📞 گام بعدی:</strong> در صورت تأیید این طراحی، می‌توانیم پیاده‌سازی را با فاز ۱ (مدل داده و Migration) شروع کنیم. کدها دقیقاً مطابق الگوهای موجود پروژه (Partial Classes برای Manager، Validation با GroupValidationBuilder، و Exception Handling با XException) نوشته خواهند شد.
</div>
</div>
</div>
<div class="page-info">
این مستند محرمانه بوده و صرفاً جهت طراحی و پیاده‌سازی داخلی تهیه شده است. هرگونه کپی‌برداری یا افشا بدون اجازه کتبی ممنوع است.
</div>
</div>
<footer class="document-footer">
<div class="footer-brand">طراحی و توسعه: هادی خزاعی اصل | شرکت فن‌آوران ساحر علم</div>
<div class="footer-divider"></div>
<div>تاریخ تهیه: ۱۲ مهر ۱۴۰۵ &nbsp;|&nbsp; نسخه: ۱.۰ &nbsp;|&nbsp; طبقه‌بندی: محرمانه</div>
</footer>
</div>
</body>
</html>