یک بار در بازبینی امنیتی یک API، با کدی روبه‌رو شدم که JWT را دقیقاً همان‌طور که در آموزش‌های ساده توضیح داده می‌شود پیاده کرده بود: توکن با HS256، طول عمر یک ساله، امضا با یک کلید ساده در فایل env. کد کار می‌کرد، ولی وقتی به تیم گفتم این پیاده‌سازی معادل درِ باز یک خانه است، اول باور نکردند. مشکل در انتخاب JWT نبود؛ در نبود تفکر معماری پشت آن بود.

در این راهنما، همان مسیری را که در پروژه‌های واقعی برای پیاده‌سازی صحیح JWT (JSON Web Token) طی می‌کنم گام‌به‌گام می‌گویم: از ساختار پایه و الگوریتم امضا تا مدیریت Refresh Token، مکانیزم Revocation و اشتباهاتی که به بدهی امنیتی تبدیل می‌شوند. اگر تازه با مفهوم JWT آشنا می‌شوید، پیشنهاد می‌کنم ابتدا JWT چیست و چه کاربردی در احراز هویت دارد؟ را بخوانید.

مفهوم JSON Web Token در استاندارد RFC 7519 تعریف شده و پیاده‌سازی صحیح آن، بر پایه همان استاندارد بنا می‌شود.

JWT دقیقاً چیست و چرا در API مدرن جایگاه ویژه دارد؟

JWT یک استاندارد باز برای انتقال امن اطلاعات بین دو طرف، به‌صورت یک توکن امضاشده و خودکفا است. خودکفا یعنی توکن حاوی تمام اطلاعاتی است که سرور برای تأیید هویت کاربر لازم دارد. این ویژگی، JWT را از توکن‌های تصادفی مرسوم جدا می‌کند و منشأ اصلی جذابیتش در معماری‌های API امروزی است.

در معماری مدرن، جداسازی فرانت‌اند و بک‌اند به یک استاندارد تبدیل شده است. فرانت‌اند می‌تواند یک اپلیکیشن موبایل، یک SPA یا یک سرویس شخص ثالث باشد. در این معماری، جریان احراز هویت مبتنی بر Session سنتی که به کوکی وابسته بود، کار نمی‌کند؛ چون کلاینت‌ها در سرورهای مختلف و دامنه‌های متفاوت قرار دارند. اینجاست که JWT با ماهیت stateless خود، جایگاهش را پیدا می‌کند.

مفهوم API به‌عنوان لایه تبادل داده، در مقاله API چیست و چه کاربردی دارد؟ آمده است؛ اگر تازه با API آشنا می‌شوید، قبل از ادامه آن را بخوانید. همچنین مقایسه JWT با OAuth به‌عنوان دو رویکرد متفاوت به احراز هویت، در OAuth چیست و چگونه کار می‌کند؟ تحلیل شده است.

جذابیت اصلی JWT در سه ویژگی خلاصه می‌شود: عدم نیاز به ذخیره‌سازی وضعیت در سرور، قابلیت استفاده در چند سرویس مختلف بدون همگام‌سازی، و امکان انتشار توکن در مرزهای جغرافیایی بدون تاخیر قابل توجه. تجربه من از پروژه‌هایی که به سمت معماری میکروسرویس رفته‌اند، این است که در همان بازه کوتاه، تیم‌ها به JWT پناه می‌برند؛ ولی همین تیم‌ها معمولاً از پیچیدگی‌های پنهان این انتخاب، کم‌آگاه‌اند.

JWT، توکن اثبات هویت است، نه جادوی امنیت؛ امنیت آن، به کیفیت پیاده‌سازی و معماری اطرافش گره خورده است.

آناتومی JWT: Header، Payload و Signature

هر JWT از سه بخش تشکیل می‌شود که با نقطه از هم جدا شده‌اند: Header، Payload و Signature. فرم خام یک JWT به شکل زیر است:

xxxxx.yyyyy.zzzzz

Header یک شیء JSON است که دو فیلد اصلی دارد: الگوریتم امضا و نوع توکن. نمونه‌ای ساده:

{
  "alg": "HS256",
  "typ": "JWT"
}

این بخش به Base64URL کد می‌شود و بخش اول توکن را می‌سازد. نکته مهم که در پروژه‌ها بارها دیده‌ام: فیلد alg قابل دست‌کاری توسط مهاجم است. اگر سمت سرور در هنگام اعتبارسنجی، الگوریتم را از خود توکن بخواند و به‌روش امن بررسی نکند، مهاجم می‌تواند الگوریتم را به `none` تغییر دهد و امضا را کاملاً حذف کند.

بخش دوم: Payload

Payload حاوی Claimها است؛ یعنی اطلاعاتی که درباره کاربر یا خودِ توکن منتقل می‌شود. سه دسته Claim وجود دارد:

  • Registered Claims: Claimهای استانداردی که در RFC 7519 تعریف شده‌اند (iss، sub، aud، exp، nbf، iat، jti)
  • Public Claims: Claimهای عمومی که در IANA Registry ثبت می‌شوند و قابل استفاده در همه جا هستند
  • Private Claims: Claimهای اختصاصی که فقط دو طرفِ تبادل از آن‌ها مطلعند

یک نکته حیاتی که در پروژه‌ها زیاد به آن برمی‌خورم: Payload کدگذاری می‌شود، ولی رمزنگاری نمی‌شود. یعنی هر کسی که توکن را در اختیار داشته باشد، می‌تواند محتوای Payload را بخواند. به همین دلیل، هرگز نباید اطلاعات حساس مثل رمز عبور یا اطلاعات کارت بانکی در Payload قرار گیرد. اصول پایه این موضوع در امنیت API در وب چگونه تأمین می‌شود؟ آمده است.

بخش سوم: Signature

Signature، بخش امضاشده توکن است و نقش آن، تضمین یکپارچگی توکن است. Signature با ترکیب Header کدشده، Payload کدشده، یک کلید مخفی (Secret) و الگوریتم مشخص تولید می‌شود. اگر هرکدام از سه بخش، حتی یک کاراکتر تغییر کند، Signature معتبر نخواهد بود.

در فرمول کلی، Signature از الگوی زیر تولید می‌شود:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

در بخش‌های بعدی، به تفصیل درباره انتخاب الگوریتم و پیاده‌سازی صحیح صحبت خواهم کرد.

چرا JWT جای Session سنتی را گرفت؟

برای درک جایگاه JWT، ابتدا باید مشکل Session سنتی را بشناسیم. در معماری Session-Based Authentication، سرور یک شناسه تصادفی به کلاینت می‌دهد و آن را در حافظه سرور (یا دیتابیس) نگه می‌دارد. هر درخواست کلاینت، همراه با این شناسه می‌آید و سرور با بررسی حافظه، هویت کاربر را تأیید می‌کند.

این مدل در معماری‌های کلاسیک وب عالی کار می‌کند، ولی در معماری‌های مدرن با چند چالش جدی روبه‌رو می‌شود:

  • مشکل مقیاس‌پذیری: هر سرور باید وضعیت تمام کاربران را در حافظه خود نگه دارد؛ افزایش سرورها به همگام‌سازی پیچیده نیاز دارد
  • مشکل میکروسرویس: در معماری توزیع‌شده، سرور احراز هویت و سرورهای سرویس مختلف باید وضعیت مشترک داشته باشند
  • مشکل موبایل: اپلیکیشن‌های موبایل، ذاتاً کوکی‌محور نیستند و کار با کوکی در آن‌ها پیچیده است
  • مشکل چند دامنه‌ای: سرویس‌هایی که در دامنه‌های مختلف قرار دارند، نمی‌توانند کوکی را به‌سادگی به اشتراک بگذارند

JWT این مشکلات را با ماهیت stateless خود حل می‌کند: سرور نیازی به نگهداری وضعیت ندارد، چون همه اطلاعات لازم در خودِ توکن است. هر سرویس، با داشتن کلید عمومی یا مشترک، می‌تواند به‌طور مستقل توکن را اعتبارسنجی کند. این ویژگی، به‌خصوص در معماری‌های توزیع‌شده، مزیت بی‌رقیبی است.

اما این مزیت، بهای خودش را دارد. JWT همیشه جایگزین بهتری برای Session نیست؛ در سایت‌های تک‌سروری با کاربران احراز‌شده محدود، Session همچنان انتخاب ساده‌تر و امن‌تری است. اگر تازه شروع کرده‌اید و می‌خواهید انتخاب آگاهانه‌ای داشته باشید، مقاله بهترین روش‌های احراز هویت کاربران کدامند؟ چارچوب کاملی ارائه می‌دهد.

الگوریتم‌های امضا؛ HS256، RS256 یا ES256؟

انتخاب الگوریتم امضا، یکی از مهم‌ترین تصمیم‌های پیاده‌سازی JWT است. سه الگوریتم اصلی که در پروژه‌ها با آن‌ها کار می‌کنم:

الگوریتمنوعمزیتمناسب برای
HS256متقارنساده و سریعسرویس‌های تک‌واحد
RS256نامتقارنکلید عمومی/خصوصی جدامیکروسرویس‌ها
ES256نامتقارن (ECC)سبک‌تر و سریع‌تر از RS256اپلیکیشن‌های موبایل

HS256 (HMAC with SHA-256)

یک الگوریتم متقارن که با یک کلید مشترک، هم امضا و هم اعتبارسنجی را انجام می‌دهد. انتخاب ساده و رایج در پروژه‌های کوچک. چالش اصلی: هر سرویسی که بخواهد توکن را اعتبارسنجی کند، به همان کلید امضا دسترسی دارد. در معماری میکروسرویس، این مسئله به یک ریسک تبدیل می‌شود. کلید HS256 باید حداقل ۲۵۶ بیت تصادفی باشد؛ اکثر نمونه‌های آموزشی که در اینترنت می‌بینید، از یک رشته ساده مثل `secret` استفاده می‌کنند که از پایه نادرست است.

RS256 (RSA with SHA-256)

الگوریتمی نامتقارن که امضا را با کلید خصوصی و اعتبارسنجی را با کلید عمومی انجام می‌دهد. سرویس احراز هویت فقط کلید خصوصی را دارد و بقیه سرویس‌ها فقط کلید عمومی را. در معماری‌های توزیع‌شده، این جداسازی، تفاوت امنیتی مهمی ایجاد می‌کند. هزینه‌اش، پیچیدگی بیشتر در مدیریت و چرخش کلیدهاست.

ES256 (ECDSA with SHA-256)

نسخه مبتنی بر منحنی‌های بیضوی که برای همان سطح امنیت، کلید کوتاه‌تری نیاز دارد. در اپلیکیشن‌های موبایل که حجم داده اهمیت دارد، انتخاب مناسب‌تری است. سرعت اعتبارسنجی هم کمی بهتر از RS256 است. انتخاب پیشنهادی من برای پروژه‌های موبایل مدرن.

یک نکته که در بازبینی‌ها بارها به آن برخوردم: انتخاب الگوریتم باید در سمت سرور تثبیت شده باشد. یعنی حتی اگر توکن بگوید HS256، اگر سرویس شما بر اساس RS256 طراحی شده، باید توکن را رد کند. این دفاع ساده، جلوی حمله الگوریتم none و حمله تغییر الگوریتم را به‌طور کامل می‌گیرد. جزئیات این نوع حملات در چگونه REST API امن بسازیم؟ آمده است.

Claim‌های استاندارد و نقش آن‌ها

Claimهای استاندارد، بخش کوچکی از Payload هستند ولی نقش مهمی در امنیت ایفا می‌کنند. Claimهایی که در هر پیاده‌سازی JWT باید با دقت تنظیم شوند:

  • iss (Issuer): صادرکننده توکن؛ سرویس مقصد باید این مقدار را بررسی کند
  • sub (Subject): شناسه کاربر که توکن برای او صادر شده
  • aud (Audience): مخاطب توکن؛ در معماری چندسرویسی، حیاتی است
  • exp (Expiration): زمان انقضای توکن؛ بدون این، توکن تا ابد معتبر می‌ماند
  • nbf (Not Before): زمان شروع اعتبار توکن
  • iat (Issued At): زمان صدور توکن
  • jti (JWT ID): شناسه یکتا برای این توکن؛ در پیاده‌سازی Blacklist از این Claim استفاده می‌شود

در تجربه من، دو Claim بیشتر از بقیه نادیده گرفته می‌شوند: aud و jti. نبود aud در معماری چندسرویسی، به این معناست که توکن صادرشده برای سرویس A، می‌تواند در سرویس B هم استفاده شود. نبود jti، پیاده‌سازی Revocation را بسیار پیچیده می‌کند. حتی اگر امروز به این دو نیاز ندارید، اضافه کردنشان هزینه‌ای ندارد و آماده‌سازی برای آینده است.

نکته دیگری که در پروژه‌ها اهمیت دارد: زمان exp باید واقع‌بینانه تنظیم شود. توکن با طول عمر یک ساعته، تعادل مناسبی بین امنیت و تجربه کاربری ایجاد می‌کند. توکن با طول عمر ساعتی، سطح امنیت را کاهش می‌دهد؛ توکن با طول عمر چند ثانیه‌ای، بار اضافی روی سرور می‌آورد.

Access Token و Refresh Token؛ چرا به تنهایی کافی نیست؟

یکی از اشتباهات رایج در پیاده‌سازی JWT، استفاده از یک توکن با طول عمر بلند است. این رویکرد ساده به‌نظر می‌رسد، ولی دو مشکل جدی دارد: نمی‌توان آن را به‌راحتی Revoke کرد، و اگر توکن لو برود، مهاجم تا زمان انقضا دسترسی دارد.

الگوی درست، استفاده از دو توکن است:

Access Token

توکن کوتاه‌عمر (معمولاً ۱۵ دقیقه تا یک ساعت) که در هر درخواست API ارسال می‌شود. این توکن را در حافظه کلاینت نگه می‌دارند و اصلاً در کوکی‌های ماندگار ذخیره نمی‌کنند. هدف، محدود کردن پنجره سوءاستفاده در صورت لو رفتن است.

Refresh Token

توکن بلندعمر (معمولاً چند روز تا چند هفته) که فقط برای درخواست Access Token جدید استفاده می‌شود. این توکن را در محیط امن‌تری ذخیره می‌کنند — مثلاً در کوکی HttpOnly یا در Keychain اپلیکیشن موبایل. وقتی Access Token منقضی می‌شود، کلاینت با Refresh Token درخواست توکن جدید می‌کند.

مزیت این الگو دو چیز است: اگر Access Token لو برود، مهاجم فقط برای چند دقیقه دسترسی دارد؛ و اگر کاربری بخواهد خروج اجباری بزند (Logout)، کافی است Refresh Token را در دیتابیس باطل کند و از آن پس، Access Token جدیدی صادر نمی‌شود. الگوی کامل این مکانیزم در احراز هویت در REST API آمده است.

یک نکته عملی که در پروژه‌ها اهمیت دارد: Rotation Refresh Token. یعنی هر بار که Refresh Token استفاده می‌شود، یک Refresh Token جدید صادر و قدیمی باطل می‌شود. این رویکرد، تشخیص سوءاستفاده را ممکن می‌کند: اگر یک Refresh Token که قبلاً مصرف شده دوباره استفاده شود، به معنای دزدیده شدن آن است و می‌توان کل session را باطل کرد.

توکن کوتاه‌عمر، پنجره سوءاستفاده را کوچک می‌کند؛ Refresh Token با Rotation، امکان تشخیص سوءاستفاده را فراهم می‌کند. این دو با هم، امنیت عملی JWT را می‌سازند.

پیاده‌سازی JWT در Node.js با Express

Node.js یکی از رایج‌ترین محیط‌ها برای پیاده‌سازی JWT است. کتابخانه استاندارد، jsonwebtoken است. یک پیاده‌سازی پایه با ساختار صحیح:

// auth.js
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;

function generateAccessToken(user) {
  return jwt.sign(
    { sub: user.id, role: user.role },
    SECRET,
    { expiresIn: '15m', algorithm: 'HS256' }
  );
}

function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET, { algorithms: ['HS256'] });
  } catch (err) {
    return null;
  }
}

سه نکته مهم در این پیاده‌سازی: اول، کلید مخفی از environment variable گرفته می‌شود، نه از کد. دوم، در هنگام اعتبارسنجی، الگوریتم صریحاً مشخص شده تا حمله الگوریتم none خنثی شود. سوم، طول عمر توکن کوتاه است.

برای middleware احراز هویت:

function authMiddleware(req, res, next) {
  const header = req.headers.authorization || '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  
  const payload = verifyToken(token);
  if (!payload) return res.status(401).json({ error: 'Invalid token' });
  
  req.user = payload;
  next();
}

در پیاده‌سازی‌های واقعی، این ساختار را با بررسی مجوزدهی در سطح منبع ترکیب می‌کنم. صرفاً تأیید هویت کافی نیست؛ هر endpoint باید بررسی کند که این کاربر مشخص، اجازه دسترسی به این منبع خاص را دارد. اصول این موضوع در آسیب‌پذیری IDOR چیست و چگونه رفع می‌شود؟ آمده است.

پیاده‌سازی JWT در Django و Flask

در پایتون، دو کتابخانه اصلی برای JWT استفاده می‌شود: PyJWT به‌عنوان کتابخانه پایه، و djangorestframework-simplejwt که یک wrapper سطح‌بالا برای Django REST Framework است.

نمونه پیاده‌سازی با PyJWT در Flask:

import jwt
from datetime import datetime, timedelta, timezone

def create_access_token(user_id, role):
    payload = {
        'sub': user_id,
        'role': role,
        'iat': datetime.now(timezone.utc),
        'exp': datetime.now(timezone.utc) + timedelta(minutes=15)
    }
    return jwt.encode(payload, app.config['JWT_SECRET'], algorithm='HS256')

def verify_access_token(token):
    try:
        return jwt.decode(
            token,
            app.config['JWT_SECRET'],
            algorithms=['HS256']
        )
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

در Django، کتابخانه SimpleJWT این ساختار را به‌صورت کامل پیاده‌سازی کرده و امکان تعریف TokenObtainPairView و TokenRefreshView را فراهم می‌کند. تجربه من از پروژه‌های Django این است که استفاده از SimpleJWT به‌جای پیاده‌سازی دستی، جلوی بسیاری از خطاهای امنیتی رایج را می‌گیرد. پیاده‌سازی دستی را فقط زمانی انتخاب کنید که نیاز به سفارشی‌سازی عمیق دارید.

پیاده‌سازی JWT در PHP و Laravel

در PHP، دو کتابخانه اصلی وجود دارد: firebase/php-jwt که پایه است، و tymon/jwt-auth که به‌طور اختصاصی برای Laravel طراحی شده. برای پروژه‌های Laravel، انتخاب دوم به‌طور محسوسی سریع‌تر و ایمن‌تر است.

نمونه پایه با firebase/php-jwt:

use Firebase\JWT\JWT;

function create_token($userId, $role) {
    $payload = [
        'sub' => $userId,
        'role' => $role,
        'iat' => time(),
        'exp' => time() + 900 // 15 دقیقه
    ];
    return JWT::encode($payload, $_ENV['JWT_SECRET'], 'HS256');
}

function verify_token($token) {
    try {
        return JWT::decode($token, new Key($_ENV['JWT_SECRET'], 'HS256'));
    } catch (Exception $e) {
        return null;
    }
}

یک نکته‌ای که در پروژه‌های وردپرسی مرتبط است: وردپرس از JWT به‌طور بومی استفاده نمی‌کند، ولی می‌توان با افزونه‌هایی مثل JWT Authentication for WP REST API، احراز هویت مبتنی بر JWT را روی REST API وردپرس فعال کرد. جزئیات این موضوع در آموزش استفاده از REST API در وردپرس آمده است.

ذخیره‌سازی امن توکن سمت کلاینت

یکی از بحرانی‌ترین تصمیم‌های پیاده‌سازی JWT، محل ذخیره‌سازی توکن در سمت کلاینت است. سه گزینه اصلی وجود دارد که هرکدام مزایا و معایب خودش را دارد:

محل ذخیرهمزیتریسک اصلی
localStorageدسترسی ساده با JavaScriptآسیب‌پذیر در برابر XSS
sessionStorageپاک‌شدن با بستن تبهنوز آسیب‌پذیر در برابر XSS
کوکی HttpOnlyدسترسی‌ناپذیر از JavaScriptنیاز به محافظت CSRF

انتخاب پیشنهادی من در پروژه‌های واقعی: Access Token در حافظه JavaScript (متغیر در closure) و Refresh Token در کوکی HttpOnly با SameSite=Strict. این ترکیب، هم آسیب‌پذیری XSS را محدود می‌کند و هم CSRF را تا حد زیادی مهار می‌کند.

ذخیره در localStorage، با وجود سادگی، انتخاب پرخطری است. اگر یک آسیب‌پذیری XSS در سایت وجود داشته باشد، مهاجم می‌تواند توکن را از localStorage استخراج و به سرور خودش منتقل کند. روش‌های محافظت در برابر XSS را در حملات XSS چیست و چگونه جلوگیری کنیم؟ آورده‌ام؛ و اگر از کوکی استفاده می‌کنید، حتماً نکات CSRF را هم در CSRF چیست و چگونه از آن جلوگیری کنیم؟ مطالعه کنید.

Revocation و Blacklist؛ چالشی که کمتر به آن پرداخته می‌شود

یکی از ویژگی‌های JWT که هم مزیت و هم چالش است، عدم امکان Revocation است. توکن صادرشده تا زمان انقضا معتبر می‌ماند، چون هیچ نقطه مرکزی برای بررسی وضعیت آن وجود ندارد. این ویژگی در معماری stateless عالی است، ولی در سناریوهای واقعی مثل خروج کاربر یا تشخیص سوءاستفاده، محدودیت جدی ایجاد می‌کند.

سه رویکرد عملی برای مدیریت Revocation:

کوتاه کردن طول عمر توکن

ساده‌ترین راه‌حل: Access Token را خیلی کوتاه‌عمر کنید (مثلاً ۵ دقیقه) و برای ادامه، از Refresh Token استفاده کنید. با این کار، حداکثر پنجره سوءاستفاده، همان چند دقیقه است. عیب این روش، افزایش بار روی سرور برای صدور توکن‌های جدید است.

Blacklist کردن توکن‌های باطل‌شده

ذخیره‌سازی jti توکن‌های باطل‌شده در یک دیتابیس سریع (Redis). در هر درخواست، علاوه بر اعتبارسنجی امضا، این دیتابیس هم بررسی می‌شود. این رویکرد، stateless بودن JWT را تا حدی از بین می‌برد، ولی امنیت را بالا می‌برد.

نسخه‌گذاری سطح کاربر

در Payload یک فیلد `token_version` نگه دارید که در دیتابیس کاربر هم موجود است. هر بار که کاربر Logout می‌کند یا رمز عبور را تغییر می‌دهد، این شماره افزایش می‌یابد. در اعتبارسنجی، تطابق این دو بررسی می‌شود. این رویکرد، امکان باطل کردن همه توکن‌های یک کاربر را یک‌باره ممکن می‌کند.

در پروژه‌های عملی، معمولاً ترکیبی از این سه رویکرد استفاده می‌کنم: Access Token کوتاه، Refresh Token در دیتابیس، و نسخه‌گذاری سطح کاربر برای سناریوهای بحرانی. این ترکیب، تعادل مناسبی بین کارآمدی و امنیت ایجاد می‌کند.

دفاع در برابر حملات رایج JWT

JWT چند آسیب‌پذیری مشخص دارد که در بازبینی‌های امنیتی بارها دیده‌ام. آشنایی با این آسیب‌پذیری‌ها، اولین گام دفاع است:

حمله الگوریتم none

مهاجم الگوریتم را از HS256 به none تغییر می‌دهد و امضا را حذف می‌کند. اگر سرور، الگوریتم را از خودِ توکن بخواند و صریحاً فقط HS256 را قبول نکند، توکن جعلی پذیرفته می‌شود. دفاع: در هنگام اعتبارسنجی، الگوریتم را صریحاً مشخص کنید.

حمله تغییر الگوریتم (Algorithm Confusion)

مهاجم الگوریتم را از RS256 به HS256 تغییر می‌دهد و از کلید عمومی (که برای همه قابل دسترسی است) به‌عنوان کلید امضای HS256 استفاده می‌کند. اگر سرور، الگوریتم را از توکن بخواند، امضا معتبر می‌شود. دفاع: همان دفاع حمله none؛ تثبیت الگوریتم در سرور.

حمله Brute Force روی کلید ضعیف

اگر کلید HS256 کوتاه یا قابل‌حدس باشد، مهاجم می‌تواند با حمله دیکشنری، کلید را پیدا کند و توکن‌های جعلی بسازد. دفاع: استفاده از کلید حداقل ۲۵۶ بیت تصادفی.

ارسال توکن در URL

هرگز توکن را در Query String قرار ندهید. URLها در لاگ سرور، تاریخچه مرورگر و هدر Referer ذخیره می‌شوند. توکن باید همیشه در هدر Authorization و به‌صورت Bearer ارسال شود.

انتقال بدون TLS

هر ارسال JWT باید از روی HTTPS انجام شود. بدون TLS، مهاجم می‌تواند توکن را در مسیر شنود کند. تفاوت HTTPS و HTTP در HTTPS چیست و چه تفاوتی با HTTP دارد؟ توضیح داده شده است. اگر سایت شما هنوز روی HTTP است، این مسئله اولویت اول است، نه تنظیمات JWT.

در کنار این حملات اختصاصی JWT، توجه به هدرهای امنیتی HTTP هم ضروری است. هدرهایی مثل Strict-Transport-Security و X-Content-Type-Options، لایه‌ای اضافه از دفاع اضافه می‌کنند. راهنمای کامل در هدرهای امنیتی HTTP چه کاربردی دارند؟ آمده است.

تست پیاده‌سازی JWT پیش از انتشار

قبل از انتشار، پیاده‌سازی JWT را در چند سناریوی مشخص تست می‌کنم. این چک‌لیست، بر اساس تجربه از چند پروژه‌ای است که در آن‌ها باگ‌های ظریف JWT، بعد از انتشار کشف شده:

  1. تست الگوریتم none: با یک توکن دستکاری‌شده با الگوریتم none، درخواست بفرستید و تأیید کنید که رد می‌شود
  2. تست الگوریتم اشتباه: توکن با الگوریتم متفاوت ارسال کنید؛ نباید پذیرفته شود
  3. تست توکن منقضی‌شده: توکن با exp در گذشته بسازید و تأیید کنید که رد می‌شود
  4. تست توکن دستکاری‌شده: یک کاراکتر از Payload را تغییر دهید؛ نباید پذیرفته شود
  5. تست توکن امضاشده با کلید دیگر: با یک کلید متفاوت امضا کنید؛ باید رد شود
  6. تست Rotate Refresh Token: یک Refresh Token را دو بار استفاده کنید؛ بار دوم باید شکست بخورد
  7. تست Revocation: کاربر را Logout کنید و بلافاصله با همان توکن درخواست بفرستید؛ باید رد شود
  8. تست دسترسی بین کاربران: با توکن کاربر A، به منابع کاربر B درخواست بفرستید؛ باید رد شود

مورد هشتم، دسترسی بین کاربران، مهم‌ترین تستی است که در بازبینی‌ها آن را اجرا می‌کنم. بسیاری از پیاده‌سازی‌ها احراز هویت را درست انجام می‌دهند، ولی مجوزدهی در سطح منبع را فراموش می‌کنند. اگر می‌خواهید این اصل را در عمق درک کنید، API چیست و چه کاربردی دارد؟ و به‌ویژه بخش‌های مربوط به مجوزدهی را مطالعه کنید.

اشتباهات رایج در پیاده‌سازی JWT

در بازبینی ده‌ها پیاده‌سازی JWT، این اشتباهات بیشترین تکرار را داشته‌اند:

  1. کلید ساده در کد: استفاده از رشته‌های ساده یا کلید کوتاه
  2. طول عمر بلند Access Token: توکن‌های یک‌ساله یا ماهانه
  3. نبود Refresh Token: ساده‌سازی مفرط که امنیت را از بین می‌برد
  4. ذخیره در localStorage: آسیب‌پذیر در برابر XSS
  5. عدم بررسی aud: در معماری چندسرویسی، اجازه استفاده توکن بین سرویس‌ها
  6. نبود Revocation: عدم امکان خروج اجباری یا تشخیص سوءاستفاده
  7. اطلاعات حساس در Payload: مثل شماره کارت یا رمز عبور
  8. ارسال توکن در URL: در Query String به‌جای هدر Authorization
  9. نبود HTTPS: انتقال توکن روی اتصال ناامن
  10. اعتماد به الگوریتم اعلامی از توکن: به‌جای تثبیت الگوریتم در سرور

مورد پنجم، عدم بررسی aud، در تجربه من یکی از مخفی‌ترین اشتباهات است. اگر توکنی برای سرویس A صادر شده، ولی سرویس B هم آن را قبول کند، مهاجم می‌تواند با توکن سرویس کم‌امنیت‌تر، به سرویس حساس‌تر دسترسی پیدا کند. این مسئله در معماری‌های میکروسرویس جدی است و در بازبینی‌های اولیه زیاد دیده نمی‌شود.

مورد سوم، نبود Refresh Token، شایع‌ترین ساده‌سازی است. تیم‌ها به‌خاطر کاهش پیچیدگی، از یک توکن بلندعمر استفاده می‌کنند و بعد در ماه ششم با درخواست غیرقابل‌اجرا از طرف تیم امنیت مواجه می‌شوند: کاربر را فوراً از سیستم خارج کنید. اگر از ابتدا Refresh Token را پیاده کنید، این سناریو حل‌شدنی است.

پرسش‌های پرتکرار درباره پیاده‌سازی JWT

این بخش را برای پاسخ به سوالات پرتکرار درباره پیاده‌سازی JWT در پروژه‌های واقعی تهیه کرده‌ام.

آیا JWT جایگزین کامل Session است؟

خیر. انتخاب بین JWT و Session به معماری بستگی دارد. در سایت‌های تک‌سروری با فرانت‌اند و بک‌اند یکپارچه، Session ساده‌تر و امن‌تر است. JWT در معماری‌های توزیع‌شده، اپلیکیشن‌های موبایل و APIهای عمومی مزیت خودش را نشان می‌دهد. انتخاب بر اساس مد روز، اشتباه رایجی است.

چه الگوریتمی برای JWT انتخاب کنم؟

در سرویس‌های تک‌واحد، HS256 با کلید قوی کافی است. در معماری‌های توزیع‌شده، RS256 یا ES256 انتخاب بهتری است چون امکان انتشار کلید عمومی را بدون افشای کلید امضا فراهم می‌کند. برای اپلیکیشن‌های موبایل، ES256 به‌خاطر حجم کمتر کلید ترجیح داده می‌شود.

طول عمر Access Token چقدر باشد؟

در تجربه من، ۱۵ دقیقه تعادل مناسبی است. طول عمر کوتاه‌تر از ۵ دقیقه، بار اضافی روی سرور می‌آورد و روی تجربه کاربری اثر می‌گذارد. طول عمر بلندتر از یک ساعت، پنجره سوءاستفاده را غیرضروری بزرگ می‌کند. اگر پروژه حساس است، ۵ تا ۱۰ دقیقه منطقی است.

آیا می‌توان JWT را در localStorage ذخیره کرد؟

از نظر فنی بله، ولی از نظر امنیتی توصیه نمی‌شود. localStorage در برابر XSS آسیب‌پذیر است. اگر سایت شما هر آسیب‌پذیری XSS داشته باشد، توکن فوراً لو می‌رود. انتخاب امن‌تر: Access Token در حافظه JavaScript و Refresh Token در کوکی HttpOnly.

آیا با JWT می‌توان سرور را Stateless نگه داشت؟

JWT ماهیتاً stateless است، ولی در پروژه‌های واقعی معمولاً بخشی از وضعیت (مثل Refresh Token و Blacklist) در دیتابیس نگه داشته می‌شود. این ترکیب، تعادل بین مزیت stateless و نیازهای امنیتی واقعی است. اگر پروژه شما از پیچیدگی این ترکیب پرهیز می‌کند، احتمالاً JWT انتخاب مناسبی برایتان نیست.

چگونه از حمله Algorithm Confusion جلوگیری کنم؟

سه اقدام: اول، الگوریتم را در سمت سرور تثبیت کنید و از توکن نخوانید. دوم، از یک کتابخانه معتبر و به‌روز استفاده کنید. سوم، الگوریتم‌های جایگزین در پیکربندی کتابخانه را محدود کنید. این سه حرکت، تمام حملات رایج الگوریتم را خنثی می‌کنند.

آیا می‌توان JWT را در چند سرویس مشترک استفاده کرد؟

بله، ولی با دو شرط. اول، Claim aud باید به‌درستی تنظیم و در هر سرویس بررسی شود. دوم، کلید امضا (در HS256) یا کلید عمومی (در RS256) باید بین سرویس‌ها به‌طور امن به اشتراک گذاشته شود. در غیر این صورت، مرزهای سرویس‌ها از نظر امنیتی از بین می‌رود.

چگونه پیاده‌سازی JWT را تست کنم؟

حداقل هشت سناریو را در چک‌لیست تست قرار دهید: الگوریتم none، الگوریتم اشتباه، توکن منقضی، توکن دستکاری‌شده، امضای متفاوت، Rotate Refresh Token، Revocation و دسترسی بین کاربران. ابزارهایی مثل Postman و ابزارهای تست امنیتی مثل OWASP ZAP می‌توانند بخشی از این تست‌ها را خودکار کنند. تست دستی همچنان برای سناریوهای منطقی ضروری است.

آیا JWT برای همه پروژه‌ها مناسب است؟

خیر. در سایت‌های کوچک با تعداد کاربران محدود، Session-Based Authentication ساده‌تر و امن‌تر است. JWT در معماری‌های توزیع‌شده، اپلیکیشن‌های موبایل و APIهای عمومی که چند کلاینت متفاوت دارند، مزیت واقعی نشان می‌دهد. انتخاب ابزار بر اساس شرایط پروژه، بهتر از پیروی از مد است.

آیا آموزش‌های موجود در اینترنت برای شروع کافی است؟

اکثر آموزش‌های ساده، پیاده‌سازی ناامن را آموزش می‌دهند: کلید ساده، طول عمر بلند، ذخیره در localStorage. اگر می‌خواهید پیاده‌سازی حرفه‌ای داشته باشید، از مستندات رسمی کتابخانه‌ای که استفاده می‌کنید شروع کنید و سپس استاندارد RFC 7519 را بخوانید. تجربه نشان داده پروژه‌هایی که بر پایه آموزش‌های ساده اینترنت ساخته شده‌اند، در بازبینی امنیتی بعدی نیاز به بازنویسی گسترده دارند.

سخن پایانی: توکن امن، معماری امن

در طول سال‌ها کار با JWT، یک درس مشترک را در همه پروژه‌ها دیده‌ام: JWT به‌تنهایی نه امن است و نه ناامن. امنیت آن، به معماری اطرافش گره خورده است. اگر Access Token کوتاه‌عمر، Refresh Token با Rotation، الگوریتم نامتقارن در معماری توزیع‌شده، ذخیره‌سازی امن در کلاینت و مکانیزم Revocation را به‌درستی پیاده کنید، JWT به یک ستون امنیتی محکم تبدیل می‌شود. اگر این‌ها را نادیده بگیرید، حتی پیاده‌سازی سطح پایین‌تر از Session هم می‌تواند امن‌تر باشد.

پیشنهاد ساده من برای شروع: از یک کتابخانه معتبر استفاده کنید، کلید را از environment variable بگیرید، الگوریتم را صریحاً تثبیت کنید، و طول عمر توکن را کوتاه نگه دارید. این چهار حرکت پایه، ۸۰ درصد خطاهای رایج را حذف می‌کند. برای بقیه، از چک‌لیست این مقاله استفاده کنید.

اگر در پروژه‌ای با یک آسیب‌پذیری JWT روبه‌رو شده‌اید — مثلاً توکنی که بدون الگوریتم پذیرفته شده، یا Refresh Tokenی که بعد از Logout معتبر مانده — تجربه‌تان را در دیدگاه‌ها بنویسید. همین جزئیات، برای کسی که امروز اولین پیاده‌سازی JWT خودش را می‌سازد، از هر مستند رسمی ارزشمندتر است. 🔐