JWT Decoder JavaScript — atob() وTextDecoder وjose
استخدم فك تشفير JWT المجاني مباشرةً في متصفحك — لا حاجة للتثبيت.
جرّب فك تشفير JWT أونلاين ←كل مسار مصادقة بنيته يصل في نهاية المطاف إلى نفس النقطة: لديك JWT في كوكي أو رأس HTTP أو عنوان URL لرد OAuth، وتحتاج لقراءة ما بداخله. فك ترميز JWT في JavaScript لا يتطلب أي حزمة npm. الـ header والـ payload في الرمز هما مجرد JSON مُرمَّز بـ Base64url، والمتصفح و Node.js يأتيان مع كل ما هو مطلوب لفك ترميزهما. يغطي هذا الدليل العملية كاملةً: تقسيم الرمز، تطبيع base64url إلى Base64 معياري، atob() و TextDecoder للتعامل الصحيح مع UTF-8، وأسلوب Buffer.from() في Node.js، والتحقق من التوقيع مع jose، والأخطاء الشائعة التي تعثر فيها المطورون يومياً. للفحص السريع لمرة واحدة، جرّب فاحص JWT أونلاين عوضاً عن ذلك. جميع الأمثلة تستهدف ES2020+ و Node.js 18+.
- ✓قسّم JWT على "." — الفهرس 0 هو الـ header، والفهرس 1 هو الـ payload، والفهرس 2 هو التوقيع.
- ✓تُفكّ atob() ترميز Base64 لكنها تُعيد سلسلة Latin-1 وليس UTF-8. استخدم TextDecoder أو Buffer.from() للمطالبات غير ASCII.
- ✓Buffer.from(segment, "base64url") يتعامل مع base64url بشكل أصلي في Node.js — لا حاجة لاستبدال حروف يدوي.
- ✓فك الترميز ليس تحققاً. لا تثق أبداً بمطالبات من JWT مفكوك الترميز دون التحقق من التوقيع على الخادم.
- ✓مكتبة jose تجمع الأمرين: تتحقق من توقيعات HS256/RS256/ES256 وتُعيد الـ payload المفكوك في استدعاء واحد.
ما هو فك ترميز JWT؟
رمز JSON Web Token هو ثلاثة مقاطع مُرمَّزة بـ Base64url مفصولة بنقاط. المقطع الأول هو الـ header، والثاني هو الـ payload (المطالبات التي تهمك فعلاً)، والثالث هو التوقيع المشفر. الـ header كائن JSON صغير يصف الرمز نفسه. أهم حقل فيه هو alg — خوارزمية التوقيع (مثلاً، HS256، RS256، ES256). حقل typ يكون دائماً تقريباً "JWT"، وحقل kid الاختياري يحدد أي مفتاح استُخدم لتوقيع الرمز — أمر بالغ الأهمية عندما يدوّر مزوّد الهوية المفاتيح وينشر نقطة نهاية JWKS مع مفاتيح عامة متعددة.
يحمل الـ payload المطالبات. يعرّف RFC 7519 سبعة أسماء مطالبات مسجّلة: sub (الموضوع — عادةً معرّف المستخدم)، iss (المُصدِر — عنوان URL لخادم المصادقة)، aud (الجمهور — واجهة API التي يُخصَّص الرمز لها)، iat (طابع زمني للإصدار)، exp (طابع زمني للانتهاء)، nbf (طابع زمني للبداية)، و jti (معرّف JWT — يُستخدم لمنع هجمات إعادة التشغيل). جميع الطوابع الزمنية بالثواني منذ Unix epoch، وليس بالمللي ثانية. مقطع التوقيع ثنائي خام — ملخص HMAC ذو مفتاح أو توقيع رقمي غير متماثل. إنه مُرمَّز بـ Base64url مثل المقاطع الأخرى، لكن بياناته ليست JSON ولا تملك بنية يمكن قراءتها.
في الممارسة العملية، تفكّ ترميز JWTs في JavaScript لثلاثة أسباب شائعة. أولاً، التصحيح: لديك رمز من مسار OAuth أو بيئة اختبار وتريد التأكد من أن المطالبات تطابق ما يجب أن يكون خادم المصادقة قد أصدره. ثانياً، قراءة مطالبات المستخدم لأغراض العرض على جانب العميل — إظهار اسم المستخدم المسجّل دخوله، عنوان URL للصورة الرمزية، أو شارة الدور من الـ payload دون طلب API إضافي. ثالثاً، التحقق من انتهاء الصلاحية قبل محاولة التجديد: إذا كان exp ضمن الـ 60 ثانية القادمة، أطلق تجديداً صامتاً قبل طلب API التالي بدلاً من انتظار رد 401.
فك الترميز لا يتحقق مما إذا كان الرمز صالحاً أو معبوثاً به. تلك عملية منفصلة تسمى التحقق، وتتطلب سر HMAC أو المفتاح العام RSA/ECDSA. يمكن لأي شخص فك ترميز JWT. فقط حامل المفتاح الصحيح يمكنه التحقق منه. هذا التمييز يشكّل عثرةً لكثير من المطورين، خاصةً عند بناء مسارات مصادقة على جانب العميل حيث تُعرض المطالبات المفكوكة لكن يجب عدم الثقة بها أبداً في قرارات التفويض دون تحقق من الواجهة الخلفية.
// Header
{ "alg": "HS256" }
// Payload
{
"sub": "usr_921f",
"role": "admin",
"iat": 1711610000
}eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
atob() + TextDecoder — فك ترميز JWT أصلي في المتصفح
يتكون المسار الأصلي للمتصفح لفك ترميز JWT من أربع خطوات. أولاً، قسّم سلسلة الرمز على "." للحصول على المقاطع الثلاثة. ثانياً، طبّع مقطع base64url باستبدال - بـ + و _ بـ /، ثم الحشو بحروف = حتى يصبح الطول مضاعفاً للأربعة. ثالثاً، استدعِ atob() لفك ترميز Base64 إلى سلسلة ثنائية. رابعاً، حوّل السلسلة الثنائية إلى UTF-8 سليم باستخدام TextDecoder. تلك الخطوة الأخيرة مهمة لأن atob() تُعيد Latin-1. الأحرف متعددة البايت — الإيموجي، النصوص CJK، الأحرف المُعلَّمة خارج نطاق Latin-1 — تظهر مشوّهة بدون هذه الخطوة.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
function decodeJwtPayload(jwt) {
const base64Url = jwt.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
const binary = atob(padded);
const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
const json = new TextDecoder("utf-8").decode(bytes);
return JSON.parse(json);
}
console.log(decodeJwtPayload(token));
// { sub: "usr_921f", role: "admin", iat: 1711610000 }خطوة الحشو يسهل إغفالها. تحذف JWTs حروف = اللاحقة من مقاطع Base64url لأن مواصفة JWT (RFC 7515) تعرّف base64url بدون حشو. لكن atob() في بعض محركات المتصفح تطرح خطأ InvalidCharacterError إذا لم يكن طول المدخل قابلاً للقسمة على 4. الحشو الاحترازي باستخدام padEnd() يتجنب هذه الحالة الحافة في جميع البيئات. إليك نسخة قابلة لإعادة الاستخدام تفكّ ترميز كل من الـ header والـ payload إلى كائنات منفصلة:
function decodeBase64Url(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
const binary = atob(padded);
const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}
function decodeJwt(token) {
const [headerB64, payloadB64] = token.split(".");
return {
header: JSON.parse(decodeBase64Url(headerB64)),
payload: JSON.parse(decodeBase64Url(payloadB64)),
};
}
const { header, payload } = decodeJwt(token);
console.log("Algorithm:", header.alg); // "HS256"
console.log("Subject:", payload.sub); // "usr_921f"
console.log("Role:", payload.role); // "admin"بمجرد حصولك على هاتين الدالتين، يستحق وضعهما في وحدة أدوات مساعدة مشتركة بدلاً من نسخ المنطق عبر الملفات. ملف src/lib/jwt.ts أو utils/jwt-decode.ts بشكل نوع واضح للإرجاع يجعل القصد صريحاً عبر قاعدة الكود. في TypeScript، يمكنك كتابة نوع الإرجاع كـ { header: JwtHeader; payload: JwtPayload } حيث يتضمن JwtHeader حقول alg، typ، والاختياري kid، و JwtPayload يمدّ المطالبات المسجّلة في RFC 7519 بتوقيع فهرس للمطالبات المخصصة. مركزة منطق فك الترميز تعني أنه عندما تريد لاحقاً إضافة معالجة الأخطاء (التقاط المقاطع المشوّهة) أو القياس (تسجيل أخطاء فك الترميز)، لديك مكان واحد فقط للتحديث.
TextDecoder هي ما يجعل هذا المسار آمناً للمطالبات غير ASCII. بدونها، atob() تُعيد سلسلة Latin-1 حيث تتوزع تسلسلات UTF-8 متعددة البايت عبر حروف. ستُشاهد نصاً هراءً بدلاً من الإيموجي أو النص CJK. مرّر دائماً عبر new TextDecoder("utf-8") بعد atob().فك ترميز مطالبات JWT بترميز UTF-8 مع أحرف متعددة البايت
بيانات payload في JWT هي JSON بترميز UTF-8 مُرمَّزة كـ base64url. معظم payloads تحتوي حقولاً ASCII فقط كمعرفات المستخدمين والطوابع الزمنية، لذا لا يلاحظ المطورون أبداً أن atob() تُعيد Latin-1 بدلاً من UTF-8. تظهر المشكلة في اللحظة التي تحتوي فيها مطالبة على إيموجي، أو أحرف يابانية، أو أحرف سيريلية، أو أي نقطة كود فوق U+00FF. الحل هو تحويل السلسلة الثنائية إلى مصفوفة بايت أولاً، ثم تشغيلها عبر TextDecoder.
// Simulating a JWT payload with emoji and CJK characters
const payloadObj = {
sub: "usr_e821",
display_name: "田中太郎",
team: "Platform 🚀",
region: "ap-northeast-1"
};
// Encode: object → JSON → UTF-8 bytes → base64url
const jsonStr = JSON.stringify(payloadObj);
const utf8Bytes = new TextEncoder().encode(jsonStr);
const base64 = btoa(String.fromCharCode(...utf8Bytes))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// Decode: base64url → base64 → binary string → bytes → UTF-8 string
const base64Std = base64.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64Std);
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const decoded = new TextDecoder("utf-8").decode(bytes);
const result = JSON.parse(decoded);
console.log(result.display_name); // "田中太郎" — صحيح
console.log(result.team); // "Platform 🚀" — صحيحثمة نمط احتياطي قديم ستراه في قواعد الكود الأقدم يستخدم decodeURIComponent مع حيلة الترميز المئوي. يعمل هذا الأسلوب decodeURIComponent في JavaScript لأنه يُعيد ترميز كل بايت كزوج هيكس مئوي، ثم decodeURIComponent يُعيد تجميع تسلسلات UTF-8 متعددة البايت:
function decodeBase64UrlLegacy(segment) {
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(base64);
// Convert each char to %XX hex, then decodeURIComponent reassembles UTF-8
const utf8 = decodeURIComponent(
binary.split("").map(c =>
"%" + c.charCodeAt(0).toString(16).padStart(2, "0")
).join("")
);
return utf8;
}
// Works for non-ASCII claims without TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));decodeURIComponent(escape(atob(segment))) القديم في مقتطفات JWT الأدوات القديمة. دالة escape() مهجورة وغير معيارية. استبدلها بأسلوب TextDecoder المُوضَّح أعلاه. نمط فك الترميز باستخدام unescape في JavaScript لديه نفس المشكلة: unescape() مهجورة أيضاً. قد تُزال كلتا الدالتين من محركات JavaScript المستقبلية.مرجع خطوات مسار فك ترميز JWT
كل خطوة في مسار فك ترميز JWT الأصلي للمتصفح، مع واجهة JavaScript البرمجية المستخدمة وما تنتجه:
المكافئ في Node.js يضغط الخطوات من 2 إلى 4 في استدعاء واحد: Buffer.from(segment, "base64url").toString("utf-8"). خيار الترميز "base64url" يتعامل مع تحويل الأبجدية والحشو داخلياً.
Buffer.from() — فك ترميز JWT في Node.js
لدى Node.js مسار أبسط بكثير. تقبل كلاس Buffer ترميز "base64url" مباشرةً، لذا تتخطى استبدال الحروف اليدوي والحشو. سطر واحد يحوّل مقطع JWT إلى سلسلة UTF-8 ويتعامل مع الأحرف متعددة البايت بشكل صحيح دون أي خطوات إضافية.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsIm9yZyI6ImFjbWUtY29ycCIsInJvbGUiOiJiaWxsaW5nIiwiaWF0IjoxNzExNjEwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
function decodeJwt(jwt) {
const segments = jwt.split(".");
return {
header: JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8")),
payload: JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8")),
};
}
const { header, payload } = decodeJwt(token);
console.log(header);
// { alg: "HS256" }
console.log(payload);
// { sub: "usr_921f", org: "acme-corp", role: "billing", iat: 1711610000 }هذا هو الأسلوب الذي أستخدمه في كل مشروع Node.js. إنه أقصر وأسرع ويتعامل مع UTF-8 بشكل صحيح بالفعل. لا حاجة لـ TextDecoder، ولا استبدال حروف، ولا حسابات حشو. كلاس Buffer يتعامل مع أبجدية base64url بشكل أصلي، مما يُزيل فئة كاملة من الأخطاء المتعلقة باستبدال الأحرف. إذا كان كودك يحتاج للعمل في كل من المتصفح و Node.js، راجع الأسئلة الشائعة في الأسفل للاطلاع على دالة غلاف متعددة البيئات تكشف البيئة في وقت التشغيل.
إليك مثالاً أكثر اكتمالاً يُظهر كيفية استخراج مطالبات JWT الشائعة وتحويل الطوابع الزمنية إلى تواريخ قابلة للقراءة، وهو النمط الذي ستستخدمه أكثر ما يكون في الوسيط ومعالجات مسار API:
function inspectToken(token) {
const segments = token.split(".");
if (segments.length !== 3) {
throw new Error("Not a valid JWT — expected 3 dot-separated segments");
}
const header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8"));
const payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8"));
const inspection = {
algorithm: header.alg,
tokenType: header.typ || "JWT",
subject: payload.sub,
issuer: payload.iss || "(not set)",
audience: payload.aud || "(not set)",
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : "(not set)",
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : "(never)",
isExpired: payload.exp ? payload.exp < Math.floor(Date.now() / 1000) : false,
customClaims: Object.keys(payload).filter(
k => !["sub", "iss", "aud", "iat", "exp", "nbf", "jti"].includes(k)
),
};
return inspection;
}
console.log(inspectToken(process.env.ACCESS_TOKEN));
// {
// algorithm: "RS256",
// tokenType: "JWT",
// subject: "usr_921f",
// issuer: "https://auth.internal",
// audience: "billing-api",
// issuedAt: "2026-03-10T14:00:00.000Z",
// expiresAt: "2026-03-10T15:00:00.000Z",
// isExpired: true,
// customClaims: ["role", "scope", "org"]
// }في خدمات Node.js الإنتاجية، يظهر نمط فك الترميز Buffer.from() في ثلاثة أماكن متكررة. الأول هو وسيط تسجيل الطلبات: تفكّ ترميز رأس HTTP Authorization الواردة لإضافة userId و org إلى كل إدخال سجل منظّم دون رحلة شبكة إضافية إلى خادم المصادقة. الثاني هو التصحيح: تطبع مطالبات الرمز المفكوكة إلى وحدة التحكم أثناء التطوير للتأكد من إصدار النطاقات الصحيحة قبل كتابة تأكيدات الاختبار. الثالث هو تجديد الرمز الاستباقي في بوابات API. بدلاً من إعادة توجيه رمز للمراحل الأعلى والسماح للخدمة المنبعثة بإعادة 401 عند انتهاء صلاحية الرمز في منتصف الطلب، تفكّ البوابة ترميز الرمز عند الحافة، تقرأ مطالبة exp، وتُطلق تجديداً إذا كانت انتهاء الصلاحية ضمن الـ 30 ثانية القادمة. هذا يُزيل فئة من أخطاء المصادقة العابرة التي يصعب استنساخها ومحبطة للتصحيح.
"base64url" في Node.js 15.7.0. إذا كنت عالقاً على Node.js 14 أو أقدم، ارجع إلى Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") الذي يعمل بنفس الطريقة لكنه يتطلب الاستبدال اليدوي للحروف.فك ترميز JWT من ملف أو استجابة API
سيناريوان يظهران باستمرار. الأول هو قراءة JWT من ملف محلي: رمز محفوظ أثناء التطوير، تركيبة اختبار، أو ملف مُفرَّغ أثناء حادثة لتحليل ما بعد الحادثة. الثاني هو استخراج JWT من استجابة HTTP، عادةً حقل access_token في جسم استجابة رمز OAuth أو رأس Authorization. كلاهما يحتاج معالجة أخطاء لأن الرموز المشوّهة والملفات المقطوعة وأخطاء الشبكة واقع يومي. قد يكون الرمز الصالح الأسبوع الماضي ذا مسافات بيضاء لاحقة أو أسطر جديدة من النسخ واللصق. قد يكون جسم الاستجابة HTML بدلاً من JSON إذا أعاد خادم المصادقة صفحة خطأ.
قراءة JWT من ملف (Node.js)
import { readFileSync } from "node:fs";
function decodeJwtFromFile(filePath) {
const raw = readFileSync(filePath, "utf-8").trim();
const segments = raw.split(".");
if (segments.length !== 3) {
throw new Error(`Invalid JWT: expected 3 segments, got ${segments.length}`);
}
try {
return {
header: JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8")),
payload: JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8")),
};
} catch (err) {
throw new Error(`Failed to decode JWT from ${filePath}: ${err.message}`);
}
}
try {
const { header, payload } = decodeJwtFromFile("./test-fixtures/access-token.txt");
console.log("Algorithm:", header.alg);
console.log("Expires:", new Date(payload.exp * 1000).toISOString());
} catch (err) {
console.error(err.message);
}استخراج JWT من استجابة API (fetch)
async function fetchAndDecodeToken(loginUrl, credentials) {
const response = await fetch(loginUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status} ${response.statusText}`);
}
const { access_token } = await response.json();
if (!access_token || access_token.split(".").length !== 3) {
throw new Error("Response does not contain a valid JWT");
}
const payload = access_token.split(".")[1];
const json = Buffer.from(payload, "base64url").toString("utf-8");
return JSON.parse(json);
}
// Usage
try {
const claims = await fetchAndDecodeToken(
"https://auth.internal/oauth/token",
{ username: "deploy-bot", password: process.env.DEPLOY_TOKEN }
);
console.log("Token subject:", claims.sub);
console.log("Token scopes:", claims.scope);
console.log("Expires at:", new Date(claims.exp * 1000).toISOString());
} catch (err) {
console.error("Token decode error:", err.message);
}فك ترميز JWT من سطر الأوامر
أحياناً تريد فقط إلقاء نظرة على رمز من الطرفية دون كتابة سكريبت. Node.js متاح في معظم أجهزة المطورين، لذا يعمل سطر واحد بشكل جيد. jq يتولى الطباعة المنسّقة.
# Decode JWT payload with Node.js one-liner
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiJ9.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
| cut -d. -f2 \
| node -e "process.stdin.on('data', d => console.log(JSON.parse(Buffer.from(d.toString().trim(), 'base64url').toString('utf-8'))))"
# Pipe to jq for pretty output
echo "$JWT_TOKEN" | cut -d. -f2 \
| node -e "process.stdin.on('data', d => process.stdout.write(Buffer.from(d.toString().trim(), 'base64url').toString('utf-8')))" \
| jq .
# Decode both header and payload
echo "$JWT_TOKEN" | node -e "
process.stdin.on('data', d => {
const parts = d.toString().trim().split('.');
console.log('Header:', JSON.parse(Buffer.from(parts[0], 'base64url').toString()));
console.log('Payload:', JSON.parse(Buffer.from(parts[1], 'base64url').toString()));
});
"إذا كنت تفضّل bash خالصاً بدون Node.js، مرّر المقطع عبر base64 -d بعد إصلاح أحرف base64url مع tr:
# Pure bash: decode JWT payload without Node.js echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq . # macOS variant (base64 -D instead of -d) echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .
للفحص المرئي السريع دون أي طرفية، الصق رمزك في أداة ToolDeck لفك ترميز JWT للحصول على تفصيل جانبي لجميع المقاطع الثلاثة مع تسميات مطالبات مرمّزة بالألوان وحالة انتهاء الصلاحية.
jose — التحقق وفك الترميز في مكتبة واحدة
لوسيط المصادقة الإنتاجي، تحتاج التحقق من التوقيع، وليس فقط فك الترميز. مكتبة jose هي الخيار الأفضل. تعمل في كل من Node.js والمتصفحات (عبر Web Crypto API)، وتدعم HS256 و RS256 و ES256 و EdDSA و JWE (الرموز المشفرة)، وتملك صفر تبعيات أصلية. ثبّتها مع npm install jose.
import * as jose from "jose";
const secret = new TextEncoder().encode("k8s-webhook-signing-secret-2026");
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInNjb3BlIjoiYmlsbGluZzpyZWFkIiwiaWF0IjoxNzExNjEwMDAwLCJleHAiOjE3MTE2MTM2MDB9.abc123";
try {
const { payload, protectedHeader } = await jose.jwtVerify(token, secret);
console.log("Algorithm:", protectedHeader.alg); // "HS256"
console.log("Subject:", payload.sub); // "usr_921f"
console.log("Scope:", payload.scope); // "billing:read"
} catch (err) {
if (err.code === "ERR_JWT_EXPIRED") {
console.error("Token expired at:", err.payload.exp);
} else {
console.error("Verification failed:", err.message);
}
}import * as jose from "jose";
// Fetch the public key set from the identity provider
const jwks = jose.createRemoteJWKSet(
new URL("https://auth.internal/.well-known/jwks.json")
);
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "Missing token" });
}
try {
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: "https://auth.internal",
audience: "billing-api",
});
// payload.sub, payload.scope, etc. are now verified
req.userId = payload.sub;
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}عند الاختيار بين jose وحزمة jsonwebtoken القديمة، الفرق الرئيسي هو نطاق وقت التشغيل. jsonwebtoken حصري لـ Node.js — يعتمد على الوحدة المدمجة crypto ولن يُحزَّم للمتصفح. أما jose فهو متعدد البيئات بالكامل: يستخدم Web Crypto API المتاحة في جميع المتصفحات الحديثة، و Node.js 16+، و Deno، و Bun، و Cloudflare Workers. إذا كان منطق المصادقة في ملف وسيط Next.js (الذي يعمل في Edge Runtime)، أو في Cloudflare Worker، أو في أداة مساعدة مشتركة تستورده الكود الخادم وكود العميل، jose هو الاختيار الصحيح لأن له صفر تبعيات أصلية ويُثبَّت دون خطوة بناء. يبقى jsonwebtoken معقولاً لتطبيقات خادم Node.js الخالص حيث تحتاج لمساعدي التوقيع الأوسع نطاقاً ولا تخطط لتشغيل الكود في بيئة حافة. في مشروع من الصفر في عام 2026، استخدم jose افتراضياً إلا إذا كان لديك سبب محدد لتفضيل واجهة API الأقدم.
إذا كنت تحتاج فك الترميز فقط بدون تحقق، تُوفّر jose jose.decodeJwt(token) التي تُعيد الـ payload و jose.decodeProtectedHeader(token) للـ header. هذه دوال مساعدة تتولى فك ترميز Base64url داخلياً. لكن السبب الكامل للوصول إلى jose هو أنك نادراً ما يجب أن تفكّ الترميز دون التحقق أيضاً. إذا كنت على جانب العميل وتحتاج فقط لإظهار اسم عرض المستخدم أو عنوان URL للصورة الرمزية من مطالبات الرمز، فك الترميز وحده مقبول. على جانب الخادم، تحقق دائماً. رأيت أنظمة إنتاجية تفكّ ترميز مطالبات JWT لقرارات التحكم في الوصول دون التحقق من التوقيع، وذلك باب مفتوح لأي مهاجم يفهم صيغة JWT.
import * as jose from "jose";
// Decode-only: no secret needed, no verification
const payload = jose.decodeJwt(token);
console.log(payload.sub); // "usr_921f"
console.log(payload.scope); // "billing:read"
const header = jose.decodeProtectedHeader(token);
console.log(header.alg); // "HS256"
console.log(header.typ); // "JWT"
// Check expiry without verification (client-side display)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
console.log("Token has expired — redirect to login");
}المخرجات الملوّنة في الطرفية
عند تصحيح رموز JWT في أداة Node.js CLI أو أثناء حادثة، تُحدث المخرجات المرمّزة بالألوان فارقاً حقيقياً. مكتبة chalk مقرونةً بـ JSON.stringify تنجز المهمة. ثبّتها مع npm install chalk.
import chalk from "chalk";
function printJwt(token) {
const segments = token.split(".");
if (segments.length !== 3) {
console.error(chalk.red("Invalid JWT: expected 3 segments"));
return;
}
const header = JSON.parse(Buffer.from(segments[0], "base64url").toString("utf-8"));
const payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf-8"));
console.log(chalk.bold.cyan("\n=== JWT Header ==="));
console.log(chalk.gray(JSON.stringify(header, null, 2)));
console.log(chalk.bold.green("\n=== JWT Payload ==="));
console.log(chalk.gray(JSON.stringify(payload, null, 2)));
// Highlight expiration status
if (payload.exp) {
const expiresAt = new Date(payload.exp * 1000);
const isExpired = expiresAt < new Date();
console.log(
chalk.bold("\nExpires:"),
isExpired
? chalk.red(`EXPIRED at ${expiresAt.toISOString()}`)
: chalk.green(`Valid until ${expiresAt.toISOString()}`)
);
}
console.log(chalk.dim("\nSignature: " + segments[2].substring(0, 20) + "..."));
}
printJwt(process.argv[2]);
// Run: node jwt-debug.mjs "eyJhbGci..."معالجة JWTs من ملفات السجل الكبيرة
تُصدر البنية التحتية الحديثة لواجهات API سجلات وصول منظّمة بتنسيق NDJSON — كائن JSON واحد لكل سطر، مع احتواء كل سطر على مسار الطلب وحالة الاستجابة والكمون ورأس Authorization المفكوك أو الخام. في الخدمات المزدحمة تنمو هذه الملفات بسرعة: بوابة تتعامل مع 10,000 طلب في الدقيقة تُنتج أكثر من 14 مليون إدخال سجل يومياً. حالات استخدام الأمن والامتثال تتطلب بانتظام مسح هذه الملفات لاحقاً — تحديد كل طلب قدّمه حساب خدمة مخترق (تحليل ما بعد الحادثة)، التأكد من انتهاء صلاحية رموز مستخدم محدد قبل نافذة وصول للبيانات (تدقيق الامتثال)، أو استخراج المجموعة الكاملة من الموضوعات التي وصلت لنقطة نهاية حساسة خلال نافذة صيانة. نظراً لأن ملف سجل واحد يمكن أن يتجاوز عدة جيجابايت، تحميله إلى الذاكرة باستخدام readFileSync غير عملي. تُعالج تدفقات readline في Node.js الملف سطراً واحداً في كل مرة مع حمل ذاكرة ثابت، مما يجعل من العملي مسح السجلات الكبيرة اعتباطياً على حاسوب محمول عادي للمطوّرين.
لن تواجه مشكلة "الملف أكبر من أن يُحمَّل في الذاكرة" مع JWTs الفردية، إذ نادراً ما يتجاوز الرمز الواحد بضعة كيلوبايت. السيناريو الذي يظهر فعلاً هو مسح سجل وصول كبير أو مسار تدقيق لرموز JWT، وفك ترميز كل منها، واستخراج مطالبات محددة. تتعامل تدفقات Node.js مع هذا دون تحميل الملف بالكامل.
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
async function scanLogsForExpiredTokens(logPath) {
const fileStream = createReadStream(logPath, { encoding: "utf-8" });
const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
let lineCount = 0;
let expiredCount = 0;
const nowSeconds = Math.floor(Date.now() / 1000);
for await (const line of rl) {
lineCount++;
try {
const entry = JSON.parse(line);
if (!entry.authorization_token) continue;
const segments = entry.authorization_token.split(".");
if (segments.length !== 3) continue;
const payload = JSON.parse(
Buffer.from(segments[1], "base64url").toString("utf-8")
);
if (payload.exp && payload.exp < nowSeconds) {
expiredCount++;
const expDate = new Date(payload.exp * 1000).toISOString();
console.log("Line " + lineCount + ": expired token for " + payload.sub + ", exp=" + expDate);
}
} catch {
// Skip malformed lines
}
}
console.log(`\nScanned ${lineCount} lines, found ${expiredCount} expired tokens`);
}
scanLogsForExpiredTokens("./logs/api-access-2026-03.ndjson");import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
async function extractUniqueSubjects(logPath) {
const rl = createInterface({
input: createReadStream(logPath, { encoding: "utf-8" }),
crlfDelay: Infinity,
});
const subjects = new Set();
const jwtRegex = /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
for await (const line of rl) {
const matches = line.match(jwtRegex);
if (!matches) continue;
for (const token of matches) {
try {
const payload = JSON.parse(
Buffer.from(token.split(".")[1], "base64url").toString("utf-8")
);
if (payload.sub) subjects.add(payload.sub);
} catch {
// Not a valid JWT
}
}
}
console.log(`Found ${subjects.size} unique subjects:`);
for (const sub of subjects) console.log(` ${sub}`);
}
extractUniqueSubjects("./logs/gateway-2026-03.log");readFileSync سيُثبّت الذاكرة ويُطلق توقفات GC. أسلوب readline يُعالج سطراً واحداً في كل مرة مع استخدام ذاكرة ثابت.الأخطاء الشائعة
المشكلة: تُعيد atob() سلسلة Latin-1. الأحرف متعددة البايت بترميز UTF-8 (الإيموجي، CJK، الأحرف المُعلَّمة) تتوزع عبر أحرف متعددة وتظهر مشوّهة.
الحل: حوّل مخرجات atob() إلى Uint8Array، ثم مرّرها عبر new TextDecoder('utf-8').
const binary = atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"));
const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
const payload = JSON.parse(new TextDecoder("utf-8").decode(bytes));
// display_name correctly shows "田中太郎"// Breaks on non-ASCII payload claims
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name appears as "ç°ä¸å¤ªé\x83\x8E" instead of "田中太郎"المشكلة: تطرح atob() خطأ "InvalidCharacterError" لأن base64url يستخدم - و _ بدلاً من + و /.
الحل: استبدل - بـ + و _ بـ / قبل استدعاء atob(). يتعامل Buffer.from() في Node.js مع 'base64url' تلقائياً.
const segment = token.split(".")[1];
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const payload = atob(base64); // now works// Throws: InvalidCharacterError: String contains an invalid character
const payload = atob(token.split(".")[1]);المشكلة: يمكن لأي شخص إنشاء JWT بأي payload. فك الترميز يقرأ البيانات فقط — لا يُثبت أن الرمز صدر من خادم المصادقة الخاص بك.
الحل: على جانب الخادم، تحقق دائماً من التوقيع باستخدام jose.jwtVerify() أو jsonwebtoken.verify(). فك الترميز فقط مقبول لعرض مطالبات المستخدم على جانب العميل.
import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
grantAdminAccess(); // safe — signature is verified
}// DANGEROUS: decoded but not verified
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
grantAdminAccess(); // attacker can forge this
}المشكلة: قيمة exp في JWT بالثواني منذ بداية الحقبة، لكن Date.now() تُعيد المللي ثانية. المقارنة ستقول دائماً أن الرمز صالح لأن الطابع الزمني بالمللي ثانية أكبر بـ 1000 مرة.
الحل: اقسم Date.now() على 1000 وخذ الجزء الصحيح قبل المقارنة مع exp.
const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
console.log("Token is valid"); // correct comparison
}// Bug: Date.now() is milliseconds, exp is seconds
if (payload.exp > Date.now()) {
console.log("Token is valid"); // always true — wrong!
}طرق فك ترميز JWT — مقارنة سريعة
استخدم atob() + TextDecoder لفك الترميز على جانب المتصفح عندما تحتاج فقط لعرض المطالبات للمستخدم. استخدم Buffer.from() في سكريبتات Node.js وأدوات CLI. الجأ إلى jose في اللحظة التي تحتاج فيها للتحقق من توقيع، وهو أي وسيط مصادقة على الجانب الخادم. حزمة jwt-decode بديل خفيف الوزن إذا أردت واجهة برمجية بدالة واحدة لفك الترميز فقط في المتصفح. للفحص المرئي السريع دون كتابة كود، الصق رمزك في أداة فك ترميز JWT.
الأسئلة الشائعة
كيف أفكّ ترميز رمز JWT في JavaScript دون مكتبة خارجية؟
قسّم الرمز على "." وخذ المقطع الثاني (الـ payload)، ثم طبّق تطبيع ترميز base64url باستبدال - بـ + و _ بـ /، والحشو بـ = حتى يصبح الطول مضاعفاً للأربعة، ثم استدعِ atob() يليه TextDecoder للحصول على سلسلة JSON بترميز UTF-8. مرّر النتيجة عبر JSON.parse() وستحصل على كائن المطالبات. لا حاجة لأي حزمة npm. هذا الأسلوب يعمل في جميع المتصفحات الحديثة وفي Node.js 18+. إذا احتجت أيضاً لقراءة الـ header، طبّق نفس خطوات فك الترميز على المقطع الأول. ضع في اعتبارك أن هذا يعطيك البيانات الخام دون أي تحقق من التوقيع — عامل النتيجة للعرض فقط ما لم تتحقق من التوقيع على الخادم.
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const payload = token.split(".")[1];
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
const json = atob(base64);
const claims = JSON.parse(json);
console.log(claims);
// { sub: "usr_921f", role: "admin" }ما الفرق بين atob() و Buffer.from() لفك ترميز JWT؟
atob() هي واجهة برمجية للمتصفح تفكّ ترميز Base64 المعياري إلى سلسلة ثنائية Latin-1. لا تفهم ترميز base64url مباشرةً، لذا يجب استبدال الحروف - و _ أولاً. أما Buffer.from(segment, "base64url") فهي واجهة برمجية لـ Node.js تتعامل مع مجموعة حروف base64url بشكل أصلي وترجع Buffer يمكن استدعاء .toString("utf-8") عليه. استخدم atob() في المتصفح، و Buffer.from() في Node.js. الخيار الثالث — الأبطأ لكنه شائع تاريخياً — هو حيلة ترميز النسبة المئوية مع decodeURIComponent، لكن هذا النمط يعتمد على دالة escape() المهجورة في بعض المقتطفات القديمة ويجب تجنبه في الكود الجديد. للكود المتعدد البيئات الذي يعمل في كلتا البيئتين، تحقق من typeof Buffer !== "undefined" وتفرّع وفقاً لذلك.
// Browser
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");لماذا ينكسر atob() مع مطالبات JWT غير ASCII؟
تُرجع atob() سلسلة Latin-1 حيث يُعيَّن كل حرف لبايت واحد. تتوزع التسلسلات متعددة البايت لـ UTF-8 (الإيموجي، الأحرف الصينية اليابانية الكورية CJK، الأحرف المُعلَّمة فوق Latin-1) عبر حروف متعددة، مما ينتج عنه نص مشوّه. الحل هو تحويل السلسلة الثنائية إلى Uint8Array أولاً، ثم تمريرها إلى new TextDecoder("utf-8").decode(). تُعيد واجهة TextDecoder تجميع التسلسلات متعددة البايت بشكل صحيح. هذه المشكلة سهلة الإغفال في مرحلة التطوير لأن معظم payloads في JWT تحتوي فقط على معرفات مستخدمين ASCII وطوابع زمنية وأسماء أدوار — تظهر المشكلة فقط عندما يحتوي مطالبة على اسم عرض غير ASCII أو سلسلة محلّية. استخدم دائماً مسار TextDecoder في الكود الجديد حتى لو كانت payloads الحالية تحتوي على ASCII فقط، إذ قد تتغير المطالبات مع تطور التطبيق.
// Broken: atob returns Latin-1, multi-byte chars are garbled
const broken = atob(base64); // "ð\x9F\x8E\x89" instead of the emoji
// Fixed: convert to byte array, then TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);هل يمكنني التحقق من توقيع JWT في JavaScript؟
فك الترميز والتحقق عمليتان مختلفتان. فك الترميز يقرأ الـ payload فقط، وهو ليس مشفراً. التحقق يفحص التوقيع مقابل سر (HMAC) أو مفتاح عام (RSA/ECDSA). تدعم مكتبة jose كلتيهما في المتصفح عبر Web Crypto API وفي Node.js. حزمة jsonwebtoken تعمل في Node.js فقط. لا تثق أبداً بالمطالبات المفكوكة الترميز دون التحقق من التوقيع على الخادم. على جانب العميل، من المقبول فك ترميز JWT لقراءة اسم عرض المستخدم أو وقت انتهاء الصلاحية، لكن أي قرار تحكم في الوصول — التحقق من دور المستخدم أو صلاحيته — يجب أن يتم في كود الخادم بعد التحقق. يمكن للمهاجم الذي يفهم صيغة JWT صياغة رمز بمطالبات عشوائية وسيجتاز فحصك على جانب العميل.
import * as jose from "jose";
const secret = new TextEncoder().encode("your-256-bit-secret");
const { payload } = await jose.jwtVerify(token, secret);
console.log(payload.sub); // verified claimsكيف أتحقق من انتهاء صلاحية JWT في JavaScript؟
فكّ ترميز الـ payload واقرأ مطالبة exp، وهي طابع زمني Unix بالثواني. قارنها مع الوقت الحالي باستخدام Math.floor(Date.now() / 1000). إذا كان الوقت الحالي أكبر من exp، فالرمز منتهي الصلاحية. تذكر: قيمة exp بالثواني منذ بداية الحقبة، وليس بالمللي ثانية، لذا يجب قسمة Date.now() على 1000. من الناحية العملية، ضع فارقاً صغيراً للانحراف الزمني — التحقق مما إذا كان الرمز سينتهي خلال الـ 30 ثانية القادمة بدلاً من التحقق الصارم من الماضي يمنع حالات الحافة حيث يكون الرمز صالحاً تقنياً عند فك الترميز لكن ينتهي بحلول وقت معالجة طلب API التالي. تعامل أيضاً مع الحالة التي يكون فيها exp غائباً كلياً، مما يعني أن الرمز لا ينتهي أبداً.
function isTokenExpired(token) {
const payload = JSON.parse(
atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
);
const nowSeconds = Math.floor(Date.now() / 1000);
return payload.exp < nowSeconds;
}
console.log(isTokenExpired(myToken)); // true or falseكيف أكتب كود فك ترميز JWT متعدد البيئات يعمل في Node.js والمتصفح معاً؟
تحقق من وجود globalThis.Buffer. إذا كان موجوداً، فأنت في Node.js ويمكنك استخدام Buffer.from(segment, "base64url").toString("utf-8"). إذا لم يكن موجوداً، فأنت في متصفح ويجب استخدام atob() مع أسلوب TextDecoder. لفّ هذا الفحص في دالة decodeBase64Url واحدة واستخدمها في كل مكان. هذا الأمر مهم بشكل خاص لحزم الأدوات المساعدة ومكونات نظام التصميم وأي كود مشترك يقع في حزمة monorepo يستورده كل من مكوّن خادم Next.js ومكوّن React للمتصفح. إبقاء اكتشاف البيئة في مكان واحد يعني أنك تحتاج فقط لتحديثه في مكان واحد إذا تغير وقت التشغيل — على سبيل المثال، عندما يضيف Deno دعماً كاملاً لـ Buffer أو يتطلب وقت تشغيل حافة جديد مساراً مختلفاً للكود.
function decodeBase64Url(segment) {
if (typeof Buffer !== "undefined") {
return Buffer.from(segment, "base64url").toString("utf-8");
}
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
return new TextDecoder("utf-8").decode(bytes);
}أدوات ذات صلة
Marcus specialises in JavaScript performance, build tooling, and the inner workings of the V8 engine. He has spent years profiling and optimising React applications, working on bundler configurations, and squeezing every millisecond out of critical rendering paths. He writes about Core Web Vitals, JavaScript memory management, and the tools developers reach for when performance really matters.
Sophie is a full-stack developer focused on TypeScript across the entire stack — from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.