JWT Authentication: Security Best Practices in 2026

Secure your APIs with JWT best practices: RS256 vs HS256, HttpOnly cookies, token rotation, refresh patterns, and common vulnerabilities to avoid in 2026.

E
ECOSIRE Research and Development Team
|19 مارس 202611 دقائق قراءة2.3k كلمات|

مصادقة JWT: أفضل ممارسات الأمان في عام 2026

تتوفر رموز JSON Web Tokens في كل مكان - ولكن تحتوي معظم التطبيقات على عيب أمني خطير واحد على الأقل. سطح الهجوم أكبر مما يبدو: هجمات الارتباك الخوارزمية، وسرقة الرمز المميز عبر XSS، وفقدان التحقق من صحة انتهاء الصلاحية، والإدارة السرية غير الصحيحة هي من بين الثغرات الأمنية الأكثر شيوعًا الموجودة في أنظمة الإنتاج. الحصول على JWT بشكل صحيح لا يعني استدعاء مكتبة والمضي قدمًا؛ فهو يتطلب قرارات مدروسة في كل طبقة.

يغطي هذا الدليل دورة حياة أمان JWT الكاملة - بدءًا من تحديد خوارزمية التوقيع وبنية الرمز المميز وحتى التخزين والتدوير والإلغاء وأنماط تنفيذ NestJS في العالم الحقيقي - حتى تتمكن من إنشاء مصادقة تصمد فعليًا تحت الهجوم.

الوجبات الرئيسية

  • استخدم دائمًا RS256 (غير المتماثل) للأنظمة الموزعة؛ HS256 فقط عندما يكون خادم API هو المُصدر والمتحقق
  • قم بتخزين الرموز المميزة في ملفات تعريف الارتباط HttpOnly وSecure وSameSite=Lax - وليس في localStorage أو sessionStorage أبدًا
  • تحقق دائمًا من صحة مطالبات exp وiss وaud وalg - لا تثق أبدًا في خوارزمية none غير الموقعة
  • تنفيذ تدوير رمز التحديث: يصدر كل تحديث زوجًا جديدًا ويبطل رمز التحديث القديم
  • اجعل رمز الوصول TTL قصيرًا (15 دقيقة)؛ استخدام رموز التحديث غير الشفافة المخزنة في قاعدة البيانات
  • لا تقم مطلقًا بتخزين البيانات الحساسة (كلمات المرور وأرقام التأمين الاجتماعي ومعلومات الدفع) في حمولات JWT - الحمولات هي base64 وليست مشفرة
  • تنفيذ إلغاء الرمز المميز عبر قائمة رفض Redis أو عداد الإصدار في قاعدة البيانات
  • قم بتسجيل كافة أحداث إصدار الرمز المميز وتحديثه لمسارات تدقيق الأمان

هيكل JWT والمطالبات

يتكون JWT من ثلاثة أجزاء: الرأس، والحمولة، والتوقيع، مفصولة بالنقاط ومشفرة بـ base64url.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0MjM0NTYwMCwiZXhwIjoxNzQyMzQ2NTAwLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIn0.
[signature]

الحمولة التي تم فك تشفيرها:

{
  "sub": "user_123",
  "email": "[email protected]",
  "role": "admin",
  "iat": 1742345600,
  "exp": 1742346500,
  "iss": "https://api.example.com",
  "aud": "https://app.example.com"
}

المطالبات المطلوبة للإنتاج:

  • sub — معرف مستخدم فريد (لا ترسل بريدًا إلكترونيًا بمفردك أبدًا — تتغير رسائل البريد الإلكتروني)
  • exp — الطابع الزمني لانتهاء الصلاحية (مطلوب دائمًا)
  • iat — تم إصداره عند الطابع الزمني (اكتشاف انحراف الساعة)
  • iss — عنوان URL لجهة الإصدار (يتم التحقق من صحته مقابل جهة الإصدار المتوقعة)
  • aud — الجمهور (التحقق من الصحة لمنع إعادة استخدام الرمز المميز عبر الخدمات)
  • jti — معرف JWT (فريد لكل رمز، يتيح الإلغاء الدقيق)

RS256 vs HS256: أي الخوارزمية يجب استخدامها

هذا هو القرار الأمني ​​الأكثر تأثيرًا في تكوين JWT.

HS256 (HMAC-SHA256) — متماثل

// Both signing and verifying require the same secret
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' });
const verified = jwt.verify(token, process.env.JWT_SECRET);

الاستخدام فقط عندما: الخدمة التي تقوم بتوقيع الرموز المميزة هي نفس الخدمة التي تتحقق منها. يعد HS256 مناسبًا لواجهات برمجة التطبيقات المتجانسة ولكنه خطير في الخدمات الصغيرة - أي خدمة يمكنها التحقق من الرموز المميزة يمكنها أيضًا إنشاؤها.

RS256 (RSA-SHA256) — غير متماثل

// Sign with private key (only the auth server holds this)
const privateKey = fs.readFileSync('/secrets/jwt-private.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: 'key-2026-01' });

// Verify with public key (any service can do this safely)
const publicKey = fs.readFileSync('/secrets/jwt-public.pem');
const verified = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // NEVER omit this — prevents algorithm confusion
  issuer: 'https://auth.example.com',
  audience: 'https://api.example.com',
});

إنشاء زوج مفاتيح RSA للإنتاج:

# Generate 4096-bit RSA private key
openssl genrsa -out jwt-private.pem 4096

# Extract public key
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem

تدوير المفتاح: استخدم keyid (kid) في الرأس. نشر المفاتيح العامة الحالية في /.well-known/jwks.json. تقوم الخدمات بتخزين JWKS مؤقتًا وجلبها إلى kid غير معروف.


تخزين ملفات تعريف الارتباط HttpOnly

هذا غير قابل للتفاوض. إن تخزين الرموز المميزة في localStorage أو sessionStorage يجعلها في متناول أي JavaScript يعمل على الصفحة - بما في ذلك البرامج النصية المحقونة من هجمات XSS.

// NestJS: set HttpOnly cookie after authentication
@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
  const { accessToken, refreshToken } = await this.authService.login(dto);

  const cookieBase = {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax' as const,
    path: '/',
    domain: process.env.COOKIE_DOMAIN, // '.example.com' for subdomain sharing
  };

  res.cookie('access_token', accessToken, {
    ...cookieBase,
    maxAge: 15 * 60 * 1000, // 15 minutes
  });

  res.cookie('refresh_token', refreshToken, {
    ...cookieBase,
    maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
    path: '/auth/refresh', // Scope refresh token to only the refresh endpoint
  });

  return { message: 'Login successful' };
}
// Extract token from cookie in JWT strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (req) => req?.cookies?.access_token, // Cookie first
        ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer fallback for API clients
      ]),
      secretOrKey: configService.get('JWT_PUBLIC_KEY'),
      algorithms: ['RS256'],
      issuer: configService.get('JWT_ISSUER'),
      audience: configService.get('JWT_AUDIENCE'),
    });
  }

  async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
    // Always check token version against database
    const user = await this.usersService.findById(payload.sub);
    if (!user || user.tokenVersion !== payload.tokenVersion) {
      throw new UnauthorizedException('Token invalidated');
    }
    return { id: payload.sub, email: payload.email, role: payload.role };
  }
}

تحديث دوران الرمز المميز

لا تقم مطلقًا بإصدار رمز وصول طويل الأمد. بدلاً من ذلك، اجعل رموز الوصول قصيرة الأمد وقم بتدوير رموز التحديث عند كل استخدام.

// auth.service.ts
@Injectable()
export class AuthService {
  async refreshTokens(refreshToken: string, ipAddress: string) {
    // 1. Look up the refresh token in the database
    const storedToken = await this.db.query.refreshTokens.findFirst({
      where: and(
        eq(refreshTokens.token, this.hashToken(refreshToken)),
        eq(refreshTokens.revoked, false),
        gt(refreshTokens.expiresAt, new Date())
      ),
      with: { user: true },
    });

    if (!storedToken) {
      // Possible reuse attack — revoke all tokens for this user
      if (storedToken?.userId) {
        await this.revokeAllUserTokens(storedToken.userId);
        await this.alertService.send({
          message: `Refresh token reuse detected for user ${storedToken.userId}`,
          severity: 'high',
        });
      }
      throw new UnauthorizedException('Invalid refresh token');
    }

    // 2. Revoke the used refresh token (rotation)
    await this.db
      .update(refreshTokens)
      .set({ revoked: true, revokedAt: new Date(), revokedByIp: ipAddress })
      .where(eq(refreshTokens.id, storedToken.id));

    // 3. Issue new token pair
    const newAccessToken = this.issueAccessToken(storedToken.user);
    const newRefreshToken = await this.issueRefreshToken(
      storedToken.user.id,
      ipAddress,
      storedToken.family // Track token families for reuse detection
    );

    return { accessToken: newAccessToken, refreshToken: newRefreshToken };
  }

  private hashToken(token: string): string {
    return crypto.createHash('sha256').update(token).digest('hex');
  }
}

مخطط قاعدة البيانات لرموز التحديث:

CREATE TABLE refresh_tokens (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token       VARCHAR(64) NOT NULL UNIQUE, -- SHA256 hash of the actual token
  family      UUID NOT NULL,               -- Token family for reuse detection
  expires_at  TIMESTAMPTZ NOT NULL,
  revoked     BOOLEAN DEFAULT false,
  revoked_at  TIMESTAMPTZ,
  revoked_by_ip INET,
  created_at  TIMESTAMPTZ DEFAULT now(),
  created_by_ip INET
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token   ON refresh_tokens(token);

استراتيجيات إلغاء الرمز المميز

تعتبر JWTs عديمة الجنسية حسب التصميم - بمجرد إصدارها، لا يمكنك "استعادتها" بدون بنية تحتية إضافية. فيما يلي ثلاث طرق مرتبة حسب المقايضة:

1. TTL قصير (15 دقيقة)

أبسط استراتيجية للإلغاء: تنتهي صلاحية رموز الوصول بسرعة كافية بحيث نادرًا ما يكون إبطالها ضروريًا. قم بالاقتران مع إلغاء رمز التحديث الفوري في قاعدة البيانات.

2. عداد إصدار الرمز المميز

قم بتخزين tokenVersion في جدول المستخدمين. قم بزيادتها لإبطال جميع الرموز الموجودة:

// Increment version to logout all sessions
await this.db
  .update(users)
  .set({ tokenVersion: sql`token_version + 1` })
  .where(eq(users.id, userId));

// In JWT payload
const payload = {
  sub: user.id,
  tokenVersion: user.tokenVersion, // embedded at sign time
};

// In JwtStrategy.validate()
if (user.tokenVersion !== payload.tokenVersion) {
  throw new UnauthorizedException();
}

يتطلب بحثًا واحدًا في قاعدة البيانات لكل طلب — مقبول لمعظم التطبيقات.

3. Redis Denylist

للإلغاء الفوري بدون عمليات بحث في قاعدة البيانات:

// Revoke specific token by JTI
async revokeToken(jti: string, expiresIn: number): Promise<void> {
  const key = `token:revoked:${jti}`;
  await this.redis.set(key, '1', 'EX', expiresIn);
}

// Check in JWT strategy before accepting
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
  const revoked = await this.redis.get(`token:revoked:${payload.jti}`);
  if (revoked) {
    throw new UnauthorizedException('Token revoked');
  }
  return this.buildUser(payload);
}

المقايضة: بحث Redis واحد لكل طلب، لكن Redis هو O(1) وأقل من مللي ثانية. مقبول لنقاط النهاية عالية الأمان.


نقاط الضعف الشائعة وعوامل التخفيف منها

هجوم الارتباك الخوارزمي

هجوم alg: "none": يقوم المهاجم بإزالة التوقيع ويضبط alg على none، ثم يرسل حمولة تم العبث بها. المكتبات التي تقبل الرموز المميزة غير الموقعة ستقبل أي حمولة.

// WRONG — never do this
jwt.verify(token, secret); // Accepts alg:none if library allows it

// CORRECT — always specify algorithms explicitly
jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Whitelist only what you use
});

حقن رأس JWT (jwk/jku)

يقوم المهاجمون بإنشاء JWT برأس jku (عنوان URL لـ JWKS) أو jwk (مفتاح مضمن) يشير إلى خادم المفاتيح الخاص بهم، ثم يقومون بالتسجيل باستخدام مفتاحهم الخاص. تقوم أداة التحقق الضعيفة بإحضار مفاتيح المهاجم وقبول الرمز المميز.

// WRONG — never fetch keys from the token header
const jwksUri = decodedHeader.jku; // Attacker-controlled!

// CORRECT — always use a pinned, config-driven JWKS URI
const jwksClient = createRemoteJWKSet(new URL(configService.get('JWKS_URI')));

أسرار ضعيفة لـ HS256

يحتوي سر ASCII المكون من 32 حرفًا لـ HS256 على حوالي 190 بت من الإنتروبيا - وهو غير كافٍ. استخدم ما لا يقل عن 256 بت من مصدر عشوائي آمن للتشفير:

# Generate a strong HS256 secret
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"

تكوين وحدة NestJS JWT

// auth.module.ts
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        privateKey: config.get('JWT_PRIVATE_KEY'),
        publicKey: config.get('JWT_PUBLIC_KEY'),
        signOptions: {
          algorithm: 'RS256',
          expiresIn: '15m',
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
        verifyOptions: {
          algorithms: ['RS256'],
          issuer: config.get('JWT_ISSUER'),
          audience: config.get('JWT_AUDIENCE'),
        },
      }),
    }),
  ],
})
export class AuthModule {}

نقطة نهاية JWKS لتوزيع المفتاح العام

// jwks.controller.ts
@Controller('.well-known')
export class JwksController {
  @Get('jwks.json')
  @Public()
  async getJwks() {
    const publicKeyPem = this.configService.get('JWT_PUBLIC_KEY');
    // Convert PEM to JWK format using the 'jose' library
    const publicKey = await importSPKI(publicKeyPem, 'RS256');
    const jwk = await exportJWK(publicKey);

    return {
      keys: [
        {
          ...jwk,
          use: 'sig',
          alg: 'RS256',
          kid: this.configService.get('JWT_KEY_ID'), // e.g., 'key-2026-01'
        },
      ],
    };
  }
}

قم بتخزين نقطة النهاية هذه بشكل مكثف - نادرًا ما تتغير المفاتيح العامة. اضبط Cache-Control: public, max-age=3600.


قائمة التحقق الأمنية

قبل نشر مصادقة JWT للإنتاج، تحقق مما يلي:

  • تم تعيين الخوارزمية بشكل صريح على RS256 أو HS256 في كل من تسجيل المكالمات والتحقق منها
  • exp، iss، aud يتم التحقق من صحته عند كل طلب
  • رمز الوصول TTL ≥ 15 دقيقة
  • الرموز المخزنة في ملفات تعريف الارتباط HttpOnly وSecure وSameSite=Lax
  • تم تنفيذ تدوير الرمز المميز - يتم إبطال الرمز المميز القديم عند كل استخدام
  • رمز التحديث المُخزن كتجزئة SHA256 في قاعدة البيانات (وليس نصًا عاديًا)
  • تمت إضافة المطالبة jti لإمكانية الإلغاء المستهدفة
  • تم فحص قائمة رفض Redis بحثًا عن نقاط نهاية عالية الأمان
  • تم نشر نقطة نهاية JWKS للخدمات الموزعة
  • المفاتيح الخاصة المخزنة في مدير الأسرار (AWS Secrets Manager، Vault)
  • تم توثيق واختبار إجراء تدوير المفاتيح
  • تم تسجيل جميع أحداث إصدار الرمز المميز وإبطاله

الأسئلة المتداولة

هل من الآمن فك تشفير JWT دون التحقق من التوقيع؟

يعد فك التشفير بدون التحقق آمنًا لقراءة الحمولة ولكن لا تثق أبدًا في محتويات الترخيص. تحقق دائمًا من التوقيع قبل التصرف بشأن المطالبات. تتخطى وظيفة jwt.decode() في معظم المكتبات التحقق — استخدمها فقط للتشخيص أو لقراءة kid من الرأس قبل تحديد المفتاح الصحيح للتحقق.

هل يجب علي استخدام ملفات تعريف الارتباط أو رؤوس التفويض لتطبيقات المتصفح؟

ملفات تعريف الارتباط HttpOnly لتطبيقات المتصفح، ورؤوس التفويض لتطبيقات الهاتف المحمول الأصلية واستدعاءات واجهة برمجة التطبيقات من خادم إلى خادم. ملفات تعريف الارتباط محصنة ضد ترشيح XSS (لا يمكن لجافا سكريبت قراءة ملفات تعريف الارتباط HttpOnly). لا يمكن لتطبيقات الهاتف المحمول استخدام ملفات تعريف الارتباط بشكل فعال واستخدام الرموز المميزة لحاملها المخزنة في مخزن المفاتيح الآمن بالجهاز.

كيف أتعامل مع انتهاء صلاحية الرمز المميز على الواجهة الأمامية؟

اعترض استجابات 401 وحاول التحديث الصامت قبل إعادة محاولة الطلب الأصلي. في React، استخدم Axios أو اجلب المعترض. إذا فشل التحديث أيضًا (انتهت صلاحيته أو تم إلغاؤه)، فأعد التوجيه لتسجيل الدخول. احتفظ بوعد تحديث واحد أثناء الرحلة لمنع عواصف التحديث الموازية.

ما الفرق بين رموز الوصول ورموز التحديث؟

رموز الوصول قصيرة العمر (15 دقيقة)، عديمة الحالة، ويتم التحقق منها باستخدام مفتاح عام أو سر مشترك في كل طلب من واجهة برمجة التطبيقات. تكون الرموز المميزة للتحديث طويلة الأمد (7-30 يومًا)، وغير شفافة (سلاسل عشوائية، وليست JWTs)، ويتم تخزينها من جانب الخادم في قاعدة البيانات. نقطة نهاية الرمز المميز للتحديث هي المكان الوحيد الذي يتم فيه استخدام الرموز المميزة للتحديث - قم بنطاقها باستخدام مسار ملف تعريف ارتباط ضيق.

هل يمكنني تخزين أدوار المستخدم في حمولة JWT؟

نعم، ولكن انتبه إلى أن الأدوار المشفرة في الرمز المميز يتم تخزينها مؤقتًا حتى انتهاء صلاحية الرمز المميز. إذا قمت بإلغاء دور المشرف الخاص بمستخدم، فسيحتفظ به حتى تنتهي صلاحية رمز الوصول الحالي الخاص به (حتى 15 دقيقة). بالنسبة لتغييرات دور الأمان العالي، أضف أيضًا المستخدم إلى قائمة Redis المرفوضة أو قم بزيادة إصدار الرمز المميز الخاص به لفرض إعادة المصادقة الفورية.

كيف يمكنني تنفيذ "تذكرني" باستخدام JWTs؟

قم بإصدار رمز تحديث طويل الأمد (90 يومًا) عندما يحدد المستخدم "تذكرني" مقابل 30 يومًا القياسي. قم بتخزين علامة persistent في صف قاعدة بيانات رمز التحديث المميز حتى تتمكن من عرض الجلسات المستمرة وإبطالها بشكل منفصل في إعدادات أمان المستخدم. لا تقم أبدًا بتوسيع رمز الوصول TTL - فهذا يتعارض مع الغرض.


الخطوات التالية

إن مصادقة JWT التي تتم بشكل صحيح هي أساس كل تطبيق ويب آمن. بدءًا من تسجيل RS256 وحتى تخزين ملفات تعريف الارتباط HttpOnly، وتدوير الرمز المميز للتحديث، واستراتيجيات الإلغاء، تحمي الأنماط الموجودة في هذا الدليل المستخدمين من هجمات المصادقة الأكثر شيوعًا.

تطبق ECOSIRE بنية مصادقة تم اختبارها في المعركة - بما في ذلك تكامل OIDC مع Authentik، وتدفقات ملفات تعريف الارتباط HttpOnly، وإدارة الرموز المميزة المدعومة من Redis - عبر جميع مشاريعنا الخلفية. استكشف خدمات التطوير التي تركز على الأمان لمعرفة كيف يمكننا تعزيز طبقة المصادقة الخاصة بك.

مشاركة:
E

بقلم

ECOSIRE Research and Development Team

بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.

الدردشة على الواتساب