JWT Decoder JavaScript — atob()، TextDecoder و jose
از رمزگشای JWT آنلاین رایگان مستقیم در مرورگرتان استفاده کنید — نیازی به نصب نیست.
امتحان کردن رمزگشای JWT آنلاین ←هر جریان احراز هویتی که ساختهام در نهایت به همان نقطه میرسد: یک JWT در یک کوکی، هدر یا URL callback OAuth دارید و باید محتوای آن را بخوانید. یک رمزگشای JWT در JavaScript نیازی به هیچ پکیج npm ندارد. هدر و بار داده توکن فقط JSON کدگذاریشده با Base64url هستند، و هم مرورگر و هم Node.js همه چیز لازم برای رمزگشایی آنها را دارند. این راهنما تمام روشهای رمزگشایی JWT در JavaScript را پوشش میدهد: تقسیم توکن، نرمالسازی base64url به Base64 استاندارد، atob() و TextDecoder برای مدیریت صحیح UTF-8، Buffer.from() در Node.js، تأیید امضا با jose، و اشتباهات رایجی که هر روز توسعهدهندگان را گرفتار میکنند. برای بررسی سریع یکباره، به جای آن از رمزگشای JWT آنلاین استفاده کنید. همه مثالها ES2020+ و Node.js 18+ را هدف میگیرند.
- ✓توکن JWT را روی "." تقسیم کنید — اندیس ۰ هدر است، اندیس ۱ بار داده است، اندیس ۲ امضا است.
- ✓atob() رمزگشایی Base64 را انجام میدهد اما Latin-1 را برمیگرداند، نه UTF-8. برای claims غیر ASCII از TextDecoder یا Buffer.from() استفاده کنید.
- ✓Buffer.from(segment, "base64url") کدگذاری base64url را به طور بومی در Node.js مدیریت میکند — نیازی به جایگزینی دستی کاراکتر نیست.
- ✓رمزگشایی تأیید نیست. بدون بررسی امضا در سمت سرور، هرگز به claims از یک JWT رمزگشاییشده اعتماد نکنید.
- ✓کتابخانه jose هر دو را انجام میدهد: امضاهای HS256/RS256/ES256 را تأیید میکند و بار داده رمزگشاییشده را در یک فراخوانی برمیگرداند.
رمزگشایی JWT چیست؟
یک JSON Web Token سه بخش کدگذاریشده با Base64url است که با نقطه از هم جدا شدهاند. بخش اول هدر است، بخش دوم بار داده است (claimsهایی که واقعاً به آنها اهمیت میدهید)، و بخش سوم امضای رمزنگاری است. هدر یک شیء JSON کوچک است که خود توکن را توصیف میکند. مهمترین فیلد آن alg است — الگوریتم امضا (مثلاً HS256، RS256، ES256). فیلد typ تقریباً همیشه "JWT" است، و فیلد اختیاری kid مشخص میکند از کدام کلید برای امضای توکن استفاده شده — هنگامی که یک identity provider کلیدها را میچرخاند و یک endpoint JWKS با چندین کلید عمومی منتشر میکند این اهمیت دارد.
بار داده claims را حمل میکند. RFC 7519 هفت نام claim ثبتشده را تعریف میکند: sub (موضوع — معمولاً شناسه کاربر)، iss (صادرکننده — URL سرور احراز هویت)، aud (مخاطب — API که توکن برای آن در نظر گرفته شده)، iat (timestamp زمان صدور)، exp (timestamp انقضا)، nbf (timestamp شروع اعتبار)، و jti (شناسه JWT — برای جلوگیری از حملات بازپخش استفاده میشود). همه timestamps بر حسب ثانیه Unix epoch هستند، نه میلیثانیه. بخش امضا باینری خام است — یک HMAC digest کلیددار یا یک امضای دیجیتال نامتقارن. مثل بخشهای دیگر با Base64url کدگذاری میشود، اما بایتهای آن JSON نیستند و ساختار قابل خواندن توسط انسان ندارند.
در عمل، شما JWTها را در JavaScript به سه دلیل رایج رمزگشایی میکنید. نخست، برای debugging — یک توکن از یک جریان OAuth یا محیط آزمایشی دارید و میخواهید تأیید کنید که claims با آنچه سرور احراز هویت باید صادر کرده باشد مطابقت دارند. دوم، خواندن claims کاربر برای اهداف نمایش در سمت کلاینت — نشان دادن نام، URL آواتار، یا نشان نقش کاربر وارد شده از بار داده توکن بدون یک API call اضافی. و سوم، بررسی انقضا پیش از تلاش برای refresh: اگر exp ظرف ۶۰ ثانیه آینده باشد، یک refresh ساکت را قبل از API call بعدی راهاندازی کنید به جای انتظار برای پاسخ ۴۰۱.
رمزگشایی بررسی نمیکند که آیا توکن معتبر یا دستکاریشده است. این یک عملیات جداگانه به نام تأیید است که نیاز به HMAC secret یا کلید عمومی RSA/ECDSA دارد. هر کسی میتواند یک JWT را رمزگشایی کند. فقط دارنده کلید صحیح میتواند آن را تأیید کند. این تمایز بسیاری از توسعهدهندگان را گرفتار میکند، به خصوص هنگام ساخت جریانهای auth سمت کلاینت که در آن claims رمزگشاییشده نمایش داده میشوند اما هرگز نباید برای تصمیمات مجوزدهی بدون بررسی تأییدشده backend مورد اعتماد قرار گیرند.
// 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 — بدون مرحله TextDecoder خراب بیرون میآیند.
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 }مرحله پَد کردن به راحتی نادیده گرفته میشود. JWTها کاراکترهای = انتهایی را از بخشهای Base64url خود حذف میکنند زیرا مشخصات JWT (RFC 7515) base64url را بدون padding تعریف میکند. اما atob() در برخی موتورهای مرورگر یک InvalidCharacterError میاندازد اگر طول ورودی بر ۴ قابل تقسیم نباشد. پَد کردن دفاعی با padEnd() از آن مورد لبهای در همه محیطها اجتناب میکند. اینجا یک نسخه قابل استفاده مجدد است که هم هدر و هم بار داده را به اشیاء جداگانه رمزگشایی میکند:
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"وقتی این دو تابع را دارید، ارزش دارد که آنها را در یک ماژول utility مشترک قرار دهید به جای کپیپیست کردن منطق در سرتاسر فایلها. یک فایل src/lib/jwt.ts یا utils/jwt-decode.ts با یک شکل بازگشتی تایپشده، قصد را در سرتاسر codebase صریح میکند. در TypeScript، میتوانید بازگشت را به صورت { header: JwtHeader; payload: JwtPayload } تایپ کنید که JwtHeader شامل alg، typ، و اختیاری kid است، و JwtPayload claims ثبتشده RFC 7519 را با یک index signature برای claims سفارشی گسترش میدهد. متمرکز کردن منطق رمزگشایی به این معناست که وقتی بعداً میخواهید مدیریت خطا (گرفتن بخشهای بدشکل) یا telemetry (ثبت شکستهای رمزگشایی) اضافه کنید، فقط یک مکان برای بهروزرسانی دارید.
TextDecoder چیزی است که این pipeline را برای claims غیر ASCII ایمن میکند. بدون آن، atob() یک رشته Latin-1 برمیگرداند که دنبالههای چند بایتی UTF-8 در کاراکترها تقسیم میشوند. به جای ایموجی یا متن CJK اشغال میبینید. همیشه پس از atob() از new TextDecoder("utf-8") عبور دهید.رمزگشایی claims JWT با کاراکترهای چند بایتی UTF-8
بار دادههای JWT به صورت JSON کدگذاریشده با UTF-8 به عنوان base64url هستند. اکثر بار دادهها شامل فیلدهای فقط ASCII مثل شناسههای کاربری و timestamps هستند، بنابراین توسعهدهندگان هرگز متوجه نمیشوند که atob() به جای UTF-8 Latin-1 برمیگرداند. مشکل لحظهای ظاهر میشود که یک claim شامل ایموجی، کاراکترهای ژاپنی، سیریلیک، یا هر code point بالای U+00FF باشد. الگوی رمزگشایی UTF-8 در JavaScript نیاز دارد که ابتدا رشته باینری را به یک آرایه بایت تبدیل کنید، سپس آن را از 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 🚀" — صحیحیک الگوی جایگزین قدیمی وجود دارد که در codebaseهای قدیمیتر میبینید که از decodeURIComponent در ترکیب با ترفند percent-encoding استفاده میکند. این رویکرد کار میکند زیرا هر بایت را به عنوان یک جفت percent-hex دوباره کدگذاری میکند، سپس 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))) را در قطعههای utility JWT قدیمیتر ببینید. تابع escape() منسوخ و غیراستاندارد است. آن را با رویکرد TextDecoder نشاندادهشده در بالا جایگزین کنید. الگوی مشابه با unescape() هم همان مشکل را دارد: این تابع هم منسوخ است. هر دو تابع ممکن است از موتورهای JavaScript آینده حذف شوند.مرجع مراحل Pipeline رمزگشایی JWT
هر مرحله در pipeline رمزگشایی JWT بومی مرورگر، با API JavaScript استفادهشده و آنچه تولید میکند:
معادل Node.js مراحل ۲ تا ۴ را در یک فراخوانی خلاصه میکند: Buffer.from(segment, "base64url").toString("utf-8"). گزینه کدگذاری "base64url" تبدیل الفبا و padding را به صورت داخلی مدیریت میکند.
Buffer.from() — رمزگشای رشته Node.js برای JWTها
Node.js مسیر بسیار سادهتری دارد. کلاس Buffer یک کدگذاری "base64url" را مستقیماً میپذیرد، بنابراین جایگزینی دستی کاراکتر و padding را رد میکنید. این روش برای کد سمت سرور است. یک خط یک بخش 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، جایگزینی کاراکتر، یا محاسبه padding نیست. کلاس Buffer الفبای base64url را به طور بومی مدیریت میکند و یک کلاس کامل از باگهای مربوط به جایگزینی کاراکتر را حذف میکند. اگر کد شما باید هم در مرورگر و هم Node.js اجرا شود، برای یک تابع wrapper ایزومورفیک که محیط را در runtime تشخیص میدهد به FAQ پایین مراجعه کنید.
اینجا یک مثال کاملتر است که نشان میدهد چطور claims رایج JWT را استخراج کنید و timestamps را به تاریخهای خوانا تبدیل کنید، که الگویی است که بیشتر اوقات در middleware و handlersهای API route استفاده خواهید کرد:
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() در سه مکان تکراری ظاهر میشود. اول middleware ثبت درخواست: هدر Authorization ورودی را رمزگشایی میکنید تا userId و org را به هر ورودی log ساختاریافته بدون یک round-trip شبکه اضافی به سرور احراز هویت اضافه کنید. دوم debugging: claims توکن رمزگشاییشده را در طول توسعه به کنسول چاپ میکنید تا تأیید کنید scopeهای صحیح قبل از نوشتن assertions آزمایشی صادر شدهاند. سوم، توکن را در API gatewayها به صورت پیشگیرانه refresh کنید. به جای ارسال یک توکن به upstream و گذاشتن سرویس downstream که ۴۰۱ برگرداند وقتی توکن در میانه درخواست منقضی میشود، gateway توکن را در لبه رمزگشایی میکند، claim exp را میخواند، و اگر انقضا ظرف ۳۰ ثانیه آینده باشد یک refresh را راهاندازی میکند. این یک دستهای از خطاهای auth گذرا را که تکثیر آنها دشوار و اشکالزداییشان ناامیدکننده است حذف میکند.
"base64url" در Node.js 15.7.0 اضافه شد. اگر روی Node.js 14 یا قبلتر گیر کردهاید، به Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") برگردید که به همان شکل کار میکند اما نیاز به جابجایی دستی کاراکتر دارد.رمزگشایی JWT از یک فایل و پاسخ API
دو سناریو به طور مداوم پیش میآید. اول خواندن یک JWT از یک فایل محلی: یک توکن ذخیرهشده در طول توسعه، یک fixture آزمایشی، یا یک فایل dump شده در طول یک حادثه برای تجزیهوتحلیل post-mortem. دوم استخراج یک JWT از یک پاسخ HTTP، معمولاً فیلد access_token در بدنه پاسخ توکن OAuth یا یک Authorization هدر. هر دو نیاز به مدیریت خطا دارند زیرا توکنهای بدشکل، فایلهای ناقص، و خطاهای شبکه واقعیتهای روزمره هستند. توکنی که هفته گذشته معتبر بود ممکن است از copy-paste فضاهای خالی یا خطوط جدید داشته باشد. بدنه پاسخ ممکن است به جای JSON یک HTML باشد اگر سرور احراز هویت یک صفحه خطا برگرداند.
خواندن 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 روی اکثر ماشینهای توسعهدهنده موجود است، بنابراین یک one-liner به خوبی کار میکند. jq pretty-printing را انجام میدهد.
# 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 استفاده کنید، بخش را پس از اصلاح کاراکترهای base64url با tr از base64 -d عبور دهید:
# 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 .
برای بازرسی بصری سریع بدون هیچ ترمینالی، توکن خود را در رمزگشای JWT ToolDeck برای یک نمای جانببهجانب از هر سه بخش با برچسبهای claim رنگبندیشده و وضعیت انقضا پیست کنید.
jose — تأیید و رمزگشایی در یک کتابخانه
برای middleware احراز هویت تولیدی، به تأیید امضا نیاز دارید، نه فقط رمزگشایی. کتابخانه jose بهترین گزینه است. هم در Node.js و هم در مرورگرها (از طریق Web Crypto API) کار میکند، از HS256، RS256، ES256، EdDSA، و JWE (توکنهای رمزگذاریشده) پشتیبانی میکند، و هیچ وابستگی native ندارد. با 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، تفاوت اصلی محدوده runtime است. jsonwebtoken فقط Node.js است — به built-in crypto متکی است و برای مرورگر bundle نمیشود. jose کاملاً ایزومورفیک است: از Web Crypto API استفاده میکند، که در همه مرورگرهای مدرن، Node.js 16+، Deno، Bun، و Cloudflare Workers موجود است. اگر منطق auth شما در یک فایل middleware Next.js (که در Edge Runtime اجرا میشود)، یا در یک Cloudflare Worker، یا در یک utility مشترک که توسط هر دو کد سرور و کلاینت import میشود قرار دارد، jose انتخاب صحیح است زیرا هیچ وابستگی native ندارد و بدون مرحله build نصب میشود. jsonwebtoken برای برنامههای سرور خالص Node.js که نیاز به ecosystem گستردهتر کمککنندههای امضا دارند و برنامهای برای اجرا در یک محیط edge ندارید منطقی باقی میماند. در یک پروژه greenfield در ۲۰۲۶، به طور پیشفرض از jose استفاده کنید مگر اینکه دلیل خاصی برای ترجیح API قدیمیتر داشته باشید.
اگر فقط به رمزگشایی بدون تأیید نیاز دارید، jose jose.decodeJwt(token) را فراهم میکند که بار داده را برمیگرداند و jose.decodeProtectedHeader(token) برای هدر. اینها توابع راحتی هستند که رمزگشایی Base64url را در داخل انجام میدهند. اما دلیل اصلی استفاده از jose این است که به ندرت باید بدون تأیید هم رمزگشایی کنید. اگر در سمت کلاینت هستید و فقط نیاز دارید نام نمایشی یا URL آواتار کاربر را از claims توکن نشان دهید، رمزگشاییتنها مناسب است. در سمت سرور، همیشه تأیید کنید. سیستمهای تولیدی دیدهام که claims 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");
}خروجی ترمینال با Syntax Highlighting
هنگام اشکالزدایی توکنهای JWT در یک ابزار CLI Node.js یا در طول یک حادثه، خروجی رنگبندیشده تفاوت واقعی ایجاد میکند. کتابخانه 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..."پردازش JWTها از فایلهای Log بزرگ
زیرساخت API مدرن لاگهای دسترسی ساختاریافته در فرمت NDJSON منتشر میکند — یک شیء JSON در هر خط، با هر خط شامل مسیر درخواست، وضعیت پاسخ، تأخیر، و هدر Authorization رمزگشاییشده یا خام. در یک سرویس پرمشغله این فایلها سریع رشد میکنند — یک gateway که ۱۰,۰۰۰ درخواست در دقیقه مدیریت میکند بیش از ۱۴ میلیون ورودی log در روز تولید میکند. موارد استفاده امنیتی و انطباق نیاز به اسکن این فایلها دارند: شناسایی درخواستهای یک حساب سرویس به خطر افتاده، تأیید انقضای توکنها پیش از یک پنجره دسترسی، یا استخراج موضوعاتی که به یک endpoint حساس دسترسی داشتند. یک فایل log ممکن است از چند گیگابایت تجاوز کند و بارگذاری آن در حافظه با readFileSync امکانپذیر نیست. جریانهای readline در Node.js فایل را یک خط در یک زمان با سربار حافظه ثابت پردازش میکنند، که اسکن لاگهای به صورت خودسرانه بزرگ را روی یک لپتاپ توسعهدهنده استاندارد عملی میکند.
مشکل "فایل برای حافظه خیلی بزرگ است" را با JWTهای منفرد به دست نخواهید آورد، زیرا یک توکن به ندرت بیش از چند کیلوبایت است. سناریویی که پیش میآید اسکن یک log دسترسی بزرگ یا trail حسابرسی برای توکنهای JWT، رمزگشایی هر یک، و استخراج claims خاص است. جریانهای 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 pause ایجاد میکند. رویکرد 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 "ç°ä¸å¤ªé\x90\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 با هر بار دادهای بسازد. رمزگشایی فقط داده را میخواند — ثابت نمیکند که توکن توسط سرور احراز هویت شما صادر شده است.
راهحل: در سمت سرور، همیشه امضا را با jose.jwtVerify() یا jsonwebtoken.verify() تأیید کنید. رمزگشاییتنها برای نمایش سمت کلاینت claims کاربر قابل قبول است.
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
}مشکل: JWT exp بر حسب ثانیه از epoch است، اما Date.now() میلیثانیه برمیگرداند. مقایسه همیشه میگوید توکن معتبر است زیرا timestamp میلیثانیه ۱۰۰۰ برابر بزرگتر است.
راهحل: Date.now() را بر ۱۰۰۰ تقسیم کنید و قبل از مقایسه با 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 برای رمزگشایی سمت مرورگر استفاده کنید وقتی فقط نیاز دارید claims را به کاربر نمایش دهید. از Buffer.from() در اسکریپتهای Node.js و ابزارهای CLI استفاده کنید. به jose برسید هر وقت نیاز به تأیید امضا دارید — یعنی در هر middleware احراز هویت سمت سرور. پکیج jwt-decode یک جایگزین سبکوزن است اگر یک API تابعتنها برای رمزگشاییتنها در مرورگر میخواهید. برای بازرسی بصری سریع بدون نوشتن کد، توکن خود را در ابزار رمزگشای JWT پیست کنید.
سوالات متداول
چطور میتوان یک توکن JWT را در JavaScript بدون کتابخانه رمزگشایی کرد؟
توکن را روی "." تقسیم کنید و بخش دوم (بار داده) را بگیرید. کدگذاری base64url را با جایگزینی - با + و _ با / نرمالسازی کنید، با = پَد کنید، سپس atob() را صدا بزنید و برای دریافت رشته JSON به UTF-8 از TextDecoder استفاده کنید. نتیجه را از JSON.parse() عبور دهید تا شیء claims را به دست آورید. هیچ پکیج npmای لازم نیست. این رویکرد در همه مرورگرهای مدرن و Node.js 18+ کار میکند. اگر نیاز به خواندن هدر هم دارید، همان مراحل رمزگشایی را برای بخش اول اعمال کنید. توجه داشته باشید که این روش دادههای خام را بدون تأیید امضا به شما میدهد — نتیجه را فقط برای نمایش استفاده کنید مگر اینکه امضا را در سمت سرور تأیید کنید.
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() یک API مرورگر است که Base64 استاندارد را به رشته باینری Latin-1 تبدیل میکند. به طور مستقیم encoding کدگذاری base64url را نمیفهمد، پس باید ابتدا کاراکترهای - و _ را جایگزین کنید. Buffer.from(segment, "base64url") یک API Node.js است که الفبای base64url را به طور بومی پشتیبانی میکند و یک Buffer برمیگرداند که میتوانید .toString("utf-8") را روی آن صدا بزنید. در مرورگر از atob() و در Node.js از Buffer.from() استفاده کنید. گزینه سوم ترفند کدگذاری درصدی 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() برای claims غیر ASCII از JWT خراب میشود؟
atob() یک رشته Latin-1 برمیگرداند که هر کاراکتر آن به یک بایت نگاشت میشود. دنبالههای چند بایتی UTF-8 (ایموجی، کاراکترهای CJK، حروف لهجهدار فراتر از Latin-1) در چندین کاراکتر تقسیم میشوند و خروجی خراب تولید میکنند. راهحل این است که ابتدا رشته باینری را به Uint8Array تبدیل کنید، سپس آن آرایه را به new TextDecoder("utf-8").decode() بدهید. API TextDecoder دنبالههای چند بایتی را به درستی بازسازی میکند. این مشکل در توسعه به راحتی نادیده گرفته میشود زیرا اکثر بار دادههای JWT فقط شناسههای کاربری ASCII، timestamps و نامهای نقش را دارند — این باگ تنها زمانی ظاهر میشود که یک claim شامل یک display_name غیر ASCII یا یک رشته بومیسازیشده باشد. همیشه در کدهای جدید از مسیر TextDecoder استفاده کنید حتی اگر بار دادههای فعلی شما فقط ASCII باشند، چون claims ممکن است با تکامل برنامه تغییر کنند.
// 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 تأیید کرد؟
رمزگشایی و تأیید، دو عملیات متفاوت هستند. رمزگشایی فقط بار داده را میخواند که رمزگذاری نشده است. تأیید، امضا را در برابر یک secret (HMAC) یا کلید عمومی (RSA/ECDSA) بررسی میکند. کتابخانه jose هر دو را در مرورگر از طریق Web Crypto API و در Node.js پشتیبانی میکند. پکیج jsonwebtoken فقط در Node.js کار میکند. هرگز به claims رمزگشاییشده بدون تأیید امضا در سمت سرور اعتماد نکنید. در سمت کلاینت، رمزگشایی JWT برای خواندن نام نمایشی یا زمان انقضا قابل قبول است. اما هر تصمیم کنترل دسترسی — اینکه آیا کاربر نقش یا مجوز خاصی دارد — باید در کد سمت سرور پس از تأیید امضا اتفاق بیفتد. یک مهاجم که فرمت JWT را میفهمد میتواند توکنی با claims دلخواه بسازد و بررسی سمت کلاینت شما پاس میشود.
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 منقضی شده است؟
بار داده را رمزگشایی کنید و claim exp را بخوانید که یک Unix timestamp بر حسب ثانیه است. آن را با زمان فعلی با استفاده از Math.floor(Date.now() / 1000) مقایسه کنید. اگر زمان فعلی از exp بزرگتر باشد، توکن منقضی شده است. به یاد داشته باشید: مقدار exp بر حسب ثانیه از epoch است، نه میلیثانیه، بنابراین تقسیم Date.now() بر ۱۰۰۰ ضروری است. در عمل، یک بافر کوچک برای اختلاف ساعت در نظر بگیرید. بررسی اینکه آیا توکن ظرف ۳۰ ثانیه آینده منقضی میشود از موارد لبهای جلوگیری میکند که توکن هنگام بررسی معتبر است اما پیش از پردازش API call بعدی منقضی میشود. همچنین حالتی را که 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 بپوشانید و همه جا از آن استفاده کنید. این مهمترین مورد برای پکیجهای utility، اجزای سیستم طراحی، و هر کد مشترکی است که در یک monorepo زندگی میکند و توسط هر دو یک سرور کامپوننت Next.js و یک کامپوننت React مرورگر import میشود. نگه داشتن تشخیص محیط در یک مکان به این معناست که تنها باید آن را در یک جا بهروزرسانی کنید اگر runtime تغییر کند — مثلاً وقتی Deno پشتیبانی کامل Buffer را اضافه میکند یا یک edge runtime جدید به مسیر کد متفاوتی نیاز دارد.
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.