ToolDeck

JavaScript JWT ถอดรหัส jose

·JavaScript Performance Engineer·ตรวจสอบโดยSophie Laurent·เผยแพร่เมื่อ

ใช้ ถอดรหัส JWT ฟรีโดยตรงในเบราว์เซอร์ของคุณ — ไม่ต้องติดตั้ง

ลอง ถอดรหัส JWT ออนไลน์ →

ทุก authentication flow ที่ผมสร้างมาจะถึงจุดเดิมเสมอ: มี JWT อยู่ใน cookie, header หรือ OAuth callback URL และจำเป็นต้องอ่านข้อมูลที่อยู่ข้างใน JWT decoder ใน JavaScript ไม่จำเป็นต้องใช้ npm package ใดเลย header และ payload ของ token เป็นแค่ Base64url-encoded JSON และทั้งเบราว์เซอร์และ Node.js มีทุกอย่างที่จำเป็นสำหรับถอดรหัสอยู่แล้ว คู่มือนี้ครอบคลุม pipeline ของ JavaScript text decoder สำหรับ JWT อย่างครบถ้วน: การแบ่ง token, การปรับ base64url ให้เป็น Base64 มาตรฐาน, atob() และ TextDecoder สำหรับจัดการ UTF-8 อย่างถูกต้อง, Buffer.from() ของ Node.js, การตรวจสอบลายเซ็นด้วย jose, และข้อผิดพลาดทั่วไปที่นักพัฒนามักเจอทุกวัน สำหรับการตรวจสอบแบบรวดเร็วครั้งเดียว ลองใช้ JWT Decoder ออนไลน์ แทน ตัวอย่างทั้งหมดใช้ ES2020+ และ Node.js 18+.

  • แบ่ง JWT ด้วย "." — index 0 คือ header, index 1 คือ payload, index 2 คือ signature
  • atob() ถอดรหัส Base64 แต่คืนค่า Latin-1 ไม่ใช่ UTF-8 ใช้ TextDecoder หรือ Buffer.from() สำหรับ claim ที่ไม่ใช่ ASCII
  • Buffer.from(segment, "base64url") จัดการ base64url ใน Node.js ได้โดยตรง ไม่ต้องแทนที่อักขระเอง
  • การถอดรหัสไม่ใช่การตรวจสอบ อย่าเชื่อ claims จาก JWT ที่ถอดรหัสแล้วโดยไม่ตรวจสอบลายเซ็นฝั่ง server
  • ไลบรารี jose ทำได้ทั้งสอง: ตรวจสอบลายเซ็น HS256/RS256/ES256 และคืนค่า payload ที่ถอดรหัสแล้วในคำสั่งเดียว

การถอดรหัส JWT คืออะไร?

JSON Web Token คือสามส่วนที่เข้ารหัส Base64url คั่นด้วยจุด ส่วนแรกคือ header ส่วนที่สอง คือ payload (claims ที่คุณต้องการ) และส่วนที่สามคือลายเซ็นเชิงรหัสลับ header คือ JSON object ขนาดเล็กที่อธิบาย token นั้นเอง field ที่สำคัญที่สุดคือ alg — อัลกอริทึมการเซ็น (เช่น HS256, RS256, ES256) field typ มักจะเป็น "JWT" เสมอ และ field ตัวเลือก kid ระบุว่าใช้ key ใดในการเซ็น token — สำคัญมากเมื่อ identity provider หมุนเวียน key และเผยแพร่ JWKS endpoint ที่มี public key หลายตัว

Payload บรรจุ claims RFC 7519 กำหนด claim name ที่ลงทะเบียนไว้เจ็ดตัว: sub (subject — มักเป็น user ID), iss (issuer — URL ของ auth server), aud (audience — API ที่ token นี้มีไว้สำหรับ), iat (timestamp ออก), exp (timestamp หมดอายุ), nbf (timestamp ใช้ได้หลังจาก) และ jti (JWT ID — ใช้ป้องกัน replay attack) timestamp ทั้งหมดเป็น Unix epoch วินาที ไม่ใช่มิลลิวินาที ส่วน signature คือ binary ดิบ — HMAC digest แบบใช้ key หรือลายเซ็นดิจิทัลแบบ asymmetric เข้ารหัสด้วย Base64url เหมือนส่วนอื่น แต่ bytes ไม่ใช่ JSON และไม่มีโครงสร้างที่มนุษย์อ่านได้

ในทางปฏิบัติ คุณถอดรหัส JWT ใน JavaScript เพื่อสามเหตุผลหลัก ประการแรกคือการ debug: มี token จาก OAuth flow หรือสภาพแวดล้อมทดสอบและต้องการยืนยันว่า claims ตรงกับที่ auth server ควรออกให้ ประการที่สองคือการอ่าน user claims เพื่อแสดงผลฝั่ง client — แสดงชื่อผู้ใช้ที่ ล็อกอิน, URL avatar หรือ role badge จาก token payload โดยไม่ต้อง API call เพิ่มเติม ประการที่สามคือตรวจสอบการหมดอายุก่อน refresh: หาก exp อยู่ในอีก 60 วินาที ให้ trigger silent refresh ก่อน API call ถัดไปแทนที่จะรอ response 401

การถอดรหัสไม่ตรวจสอบว่า token ถูกต้องหรือถูกแก้ไข นั่นเป็นขั้นตอนแยกที่เรียกว่าการตรวจสอบ ซึ่งต้องการ HMAC secret หรือ RSA/ECDSA public key ใครก็สามารถถอดรหัส JWT ได้ เฉพาะผู้ถือ key ที่ถูกต้องเท่านั้นที่สามารถตรวจสอบได้ ความแตกต่างนี้ทำให้นักพัฒนาหลายคน สับสน โดยเฉพาะเมื่อสร้าง auth flow ฝั่ง client ที่ claims ที่ถอดรหัสแล้วถูกแสดงผล แต่ต้องไม่เชื่อถือสำหรับการตัดสินใจ authorization โดยไม่ผ่านการตรวจสอบฝั่ง backend

Before · json
After · json
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOTIxZiIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTcxMTYxMDAwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
// Header
{ "alg": "HS256" }

// Payload
{
  "sub": "usr_921f",
  "role": "admin",
  "iat": 1711610000
}

atob() + TextDecoder — ถอดรหัส JWT แบบ Browser-Native

pipeline ของเบราว์เซอร์สำหรับถอดรหัส JWT มีสี่ขั้นตอน ขั้นแรก แบ่ง token string ด้วย "." เพื่อได้สามส่วน ขั้นที่สอง ปรับ base64url segment โดยแทนที่ - ด้วย + และ _ ด้วย /, แล้วเติม = จนความยาวเป็นทวีคูณของ 4 ขั้นที่สาม เรียก atob() เพื่อถอดรหัส Base64 ให้เป็น binary string ขั้นที่สี่ แปลง binary string ให้เป็น UTF-8 ที่ถูกต้องโดยใช้ TextDecoderขั้นตอนสุดท้ายนั้นสำคัญเพราะ atob() คืนค่า Latin-1 อักขระหลาย byte — emoji, ข้อความ CJK, อักขระที่มีสัญลักษณ์กำกับเกิน Latin-1 range — จะออกมาเสียหายหากไม่มีขั้นตอน JavaScript text decoder นี้

JavaScript — minimal JWT decode
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 }

ขั้นตอนการเติม padding มักถูกมองข้าม JWT ตัด = ท้ายออกจาก Base64url segments เพราะ JWT specification (RFC 7515) กำหนด base64url ไว้โดยไม่มี padding แต่ atob() ในบางเบราว์เซอร์จะ throw InvalidCharacterError หากความยาว input ไม่หาร 4 ลงตัว การเติม padding อย่างป้องกันด้วย padEnd() หลีกเลี่ยง edge case นั้นในทุกสภาพแวดล้อม นี่คือเวอร์ชันที่นำกลับมาใช้ใหม่ได้ ซึ่งถอดรหัสทั้ง header และ payload ให้เป็น object แยกกัน:

JavaScript — decode header and 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"

เมื่อมีสองฟังก์ชันนี้แล้ว ควรวางไว้ใน shared utility module แทนที่จะ copy-paste logic ข้ามไฟล์ ไฟล์ src/lib/jwt.ts หรือ utils/jwt-decode.ts ที่มี return shape แบบ typed จะทำให้ intent ชัดเจนทั่ว codebase ใน TypeScript สามารถ type return เป็น { header: JwtHeader; payload: JwtPayload } โดยที่ JwtHeader มี alg, typ และ kid ตัวเลือก และ JwtPayload ขยาย registered claims ของ RFC 7519 ด้วย index signature สำหรับ custom claims การรวม decode logic ไว้ที่เดียวหมายความว่าเมื่อต้องการเพิ่ม error handling (จัดการ segment ที่ผิดรูปแบบ) หรือ telemetry (log decode failures) ต้องอัปเดตเพียงที่เดียว

หมายเหตุ:ขั้นตอน TextDecoder คือสิ่งที่ทำให้ pipeline นี้ปลอดภัย สำหรับ claims ที่ไม่ใช่ ASCII หากไม่มีขั้นตอนนี้ atob() จะคืนค่า Latin-1 string ที่ลำดับ UTF-8 หลาย byte ถูกแยกข้ามตัวอักษร จะเห็น garbage แทน emoji หรือ CJK text ให้ส่งผ่าน new TextDecoder("utf-8") หลัง atob() เสมอ

การถอดรหัส UTF-8 JWT Claims ที่มีอักขระหลาย Byte

JWT payload คือ UTF-8 JSON ที่เข้ารหัสเป็น base64url นักพัฒนาส่วนใหญ่ไม่เคยสังเกตว่า atob() คืนค่า Latin-1 แทน UTF-8 เพราะ payload ส่วนใหญ่มีเฉพาะ field ที่เป็น ASCII เช่น user ID และ timestamp ปัญหาจะปรากฏทันทีที่ claim มี emoji, ตัวอักษรญี่ปุ่น, Cyrillic หรือ code point ใดก็ตามที่เกิน U+00FF รูปแบบ JavaScript decode UTF-8 ต้องแปลง binary string เป็น byte array ก่อน แล้วจึงรันผ่าน TextDecoder

JavaScript — UTF-8 round-trip with emoji in JWT payload
// จำลอง JWT payload ที่มี emoji และอักขระ CJK
const payloadObj = {
  sub: "usr_e821",
  display_name: "田中太郎",  // ตัวอย่างอักขระ multi-byte
  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 🚀" — ถูกต้อง

มีรูปแบบ fallback เดิมที่พบในโค้ดเก่าซึ่งใช้ decodeURIComponent ร่วมกับ percent-encoding trick แนวทาง JavaScript decodeURIComponent นี้ทำงานได้ เพราะ re-encode แต่ละ byte เป็น percent-hex pair แล้ว decodeURIComponent ประกอบลำดับ UTF-8 หลาย byte ใหม่:

JavaScript — decodeURIComponent fallback for UTF-8
function decodeBase64UrlLegacy(segment) {
  const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
  const binary = atob(base64);
  // แปลงแต่ละตัวอักษรเป็น %XX hex แล้ว decodeURIComponent ประกอบ UTF-8
  const utf8 = decodeURIComponent(
    binary.split("").map(c =>
      "%" + c.charCodeAt(0).toString(16).padStart(2, "0")
    ).join("")
  );
  return utf8;
}

// ใช้งานได้กับ non-ASCII claims โดยไม่ต้องใช้ TextDecoder
const payload = decodeBase64UrlLegacy(token.split(".")[1]);
console.log(JSON.parse(payload));
คำเตือน:คุณอาจพบรูปแบบเดิม decodeURIComponent(escape(atob(segment))) ใน JWT utility snippet เก่าๆ ฟังก์ชัน escape() ถูก deprecated และไม่เป็นมาตรฐาน ให้แทนที่ด้วยแนวทาง TextDecoder ที่แสดงไว้ข้างต้น รูปแบบ JavaScript unescape decoder มีปัญหาเดียวกัน:unescape() ถูก deprecated ทั้งสองฟังก์ชัน อาจถูกลบออกจาก JavaScript engine รุ่นใหม่

JWT Decode Pipeline — เอกสารอ้างอิงแต่ละขั้นตอน

แต่ละขั้นตอนใน browser-native JWT decode pipeline พร้อม JavaScript API ที่ใช้ และสิ่งที่ผลิตออกมา:

พารามิเตอร์ / ขั้นตอน
ประเภท
คำอธิบาย
token.split(".")
string[]
แบ่ง JWT ออกเป็น [header, payload, signature] สามส่วน
base64url → base64
string replace
แทนที่ - ด้วย +, _ ด้วย / และเติม = จนความยาวเป็นทวีคูณของ 4
atob(base64)
string
ถอดรหัส Base64 มาตรฐานให้เป็น binary string (Latin-1)
TextDecoder("utf-8")
TextDecoder
แปลง Uint8Array ของ raw bytes ให้เป็น UTF-8 string ที่ถูกต้อง
JSON.parse()
object
แปลง JSON string ผลลัพธ์ให้เป็น JavaScript object

Node.js equivalent รวบขั้นตอนที่ 2 ถึง 4 เป็น call เดียว: Buffer.from(segment, "base64url").toString("utf-8")ตัวเลือก encoding "base64url" จัดการการแปลง alphabet และ padding ภายใน

Buffer.from() — String Decoder ของ Node.js สำหรับ JWT

Node.js มีเส้นทางที่ง่ายกว่ามาก class Buffer รับ encoding "base64url" โดยตรง ทำให้ข้ามการแทนที่อักขระและ padding เอง นี่คือเส้นทาง JavaScript string decoder สำหรับโค้ดฝั่ง server บรรทัดเดียวแปลง JWT segment ให้เป็น UTF-8 string และจัดการอักขระหลาย byte ได้อย่างถูกต้องโดยไม่ต้องมีขั้นตอนเพิ่มเติม

Node.js 18+ — JWT decode with Buffer
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 project สั้นกว่า เร็วกว่า และจัดการ UTF-8 ได้ถูกต้องแล้ว ไม่ต้องใช้ TextDecoder ไม่ต้องแทนที่อักขระ ไม่ต้องคำนวณ padding class Buffer เป็น JavaScript string decoder ที่จัดการ base64url alphabet natively ซึ่งกำจัด bug ทั้งหมวดที่เกี่ยวกับการแทนที่อักขระ หากโค้ดต้องรันได้ทั้งเบราว์เซอร์และ Node.js ดู FAQ ด้านล่างสำหรับ isomorphic wrapper function ที่ตรวจจับสภาพแวดล้อมขณะ runtime

นี่คือตัวอย่างที่สมบูรณ์กว่าซึ่งแสดงวิธีดึง JWT claims ทั่วไปและแปลง timestamp ให้เป็นวันที่ที่อ่านได้ ซึ่งเป็นรูปแบบที่คุณจะใช้บ่อยที่สุดใน middleware และ API route handlers:

Node.js — practical JWT claim extraction
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"]
// }

ใน production Node.js services รูปแบบ decode ด้วย Buffer.from() ปรากฏในสามที่ซ้ำๆ กัน ที่แรกคือ request logging middleware: ถอดรหัส header Authorization ที่เข้ามาเพื่อแนบ userId และ org กับทุก log entry แบบ structured โดยไม่ต้อง round-trip เพิ่มไปยัง auth server ที่สองคือการ debug: แสดง decoded token claims ในคอนโซลระหว่างพัฒนาเพื่อยืนยันว่า scope ที่ถูกต้องถูกออกให้ก่อนเขียน test assertions ที่สามคือ proactive token refresh ใน API gateway แทนที่จะส่ง token ต่อ upstream และปล่อยให้ downstream service คืน 401 เมื่อ token หมดอายุระหว่าง request gateway ถอดรหัส token ที่ edge อ่าน claim exp และ trigger refresh หากหมดอายุในอีก 30 วินาที ซึ่งกำจัด auth failures แบบชั่วคราว ที่ยากต่อการทำซ้ำและน่าหงุดหงิดในการ debug

หมายเหตุ:encoding "base64url" ถูกเพิ่มใน Node.js 15.7.0 หากยังใช้ Node.js 14 หรือเก่ากว่า ให้ใช้ Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64") ซึ่งทำงานเหมือนกันแต่ต้องแทนที่อักขระเอง

ถอดรหัส JWT จาก File และ API Response

มีสองสถานการณ์ที่เจอบ่อย สถานการณ์แรกคือการอ่าน JWT จาก local file: token ที่บันทึกไว้ระหว่างพัฒนา, test fixture หรือไฟล์ที่ dump ระหว่าง incident เพื่อวิเคราะห์ภายหลัง สถานการณ์ที่สองคือการดึง JWT จาก HTTP response โดยทั่วไปคือ field access_token ใน OAuth token response body หรือ header Authorization ทั้งสองต้องการ error handling เพราะ token ที่ผิดรูปแบบ ไฟล์ที่ถูกตัดทอน และ network error เป็นเรื่องปกติ token ที่ใช้ได้เมื่อสัปดาห์ที่แล้วอาจมี whitespace หรือ newline จากการ copy-paste response body อาจเป็น HTML แทน JSON หาก auth server คืน error page

อ่าน JWT จาก File (Node.js)

Node.js — decode JWT from file with error handling
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 Response (fetch)

JavaScript — decode JWT from API response
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 ผ่าน Command-Line

บางครั้งต้องการดู token จาก terminal โดยไม่ต้องเขียน script Node.js มีอยู่บนเครื่อง developer ส่วนใหญ่ ดังนั้น one-liner ทำงานได้ดี jq จัดการการแสดงผลแบบ pretty-print

bash — decode JWT payload from terminal
# ถอดรหัส JWT payload ด้วย 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 ไปยัง jq เพื่อแสดงผลแบบ pretty
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 .

# ถอดรหัสทั้ง header และ 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 ให้ pipe segment ผ่าน base64 -d หลังจากแก้ไขอักขระ base64url ด้วย tr:

bash — pure bash JWT decode without Node.js
# Pure bash: ถอดรหัส JWT payload โดยไม่ใช้ Node.js
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

# สำหรับ macOS (ใช้ base64 -D แทน -d)
echo "$JWT_TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .

สำหรับการตรวจสอบด้วยตาอย่างรวดเร็วโดยไม่ต้องใช้ terminal เลย วาง token ลงใน ToolDeck JWT Decoder เพื่อดูทั้งสามส่วนแบบ side-by-side พร้อม claim labels ที่มีสีสันและสถานะการหมดอายุ

jose — ตรวจสอบและถอดรหัสในไลบรารีเดียว

สำหรับ production authentication middleware คุณต้องการการตรวจสอบลายเซ็น ไม่ใช่แค่การถอดรหัส ไลบรารี jose เป็นตัวเลือกที่ดีที่สุด ทำงานได้ทั้งใน Node.js และเบราว์เซอร์ (ผ่าน Web Crypto API) รองรับ HS256, RS256, ES256, EdDSA และ JWE (encrypted token) และไม่มี native dependency ติดตั้งด้วย npm install jose

JavaScript — jose: verify HS256 token
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);
  }
}
JavaScript — jose: verify RS256 with JWKS endpoint
import * as jose from "jose";

// ดึง public key set จาก 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 เป็นต้น ได้รับการยืนยันแล้ว
  req.userId = payload.sub;
} catch (err) {
  return res.status(401).json({ error: "Invalid token" });
}

เมื่อเลือกระหว่าง jose และ package เก่าอย่าง jsonwebtoken ความแตกต่างสำคัญคือขอบเขต runtime jsonwebtoken ใช้ได้เฉพาะ Node.js — พึ่งพา built-in crypto และไม่สามารถ bundle สำหรับเบราว์เซอร์ได้ jose เป็น isomorphic อย่างสมบูรณ์: ใช้ Web Crypto API ที่มีในเบราว์เซอร์สมัยใหม่ทุกตัว, Node.js 16+, Deno, Bun และ Cloudflare Workers หาก auth logic อยู่ใน Next.js middleware file (ซึ่งรันใน Edge Runtime) หรือใน Cloudflare Worker หรือใน shared utility ที่ถูก import โดยทั้ง server และ client code jose คือตัวเลือกที่ถูกต้องเพราะไม่มี native dependency และติดตั้งโดยไม่ต้อง build step jsonwebtoken ยังสมเหตุสมผลสำหรับ pure Node.js server application ที่ต้องการ signing helpers ที่หลากหลายกว่าและไม่ได้วางแผนรันโค้ดใน edge environment สำหรับ project ใหม่ในปี 2026 ให้เลือก jose เป็นค่าเริ่มต้น เว้นแต่มีเหตุผลเฉพาะที่ต้องการ API เก่า

หากต้องการเพียงถอดรหัสโดยไม่ตรวจสอบ jose มี jose.decodeJwt(token) ซึ่งคืนค่า payload และ jose.decodeProtectedHeader(token) สำหรับ header ฟังก์ชันเหล่านี้เป็น convenience function ที่ทำ Base64url decoding ภายใน แต่เหตุผลหลักที่จะใช้ jose คือคุณแทบจะไม่ควรถอดรหัสโดยไม่ตรวจสอบด้วย หากอยู่ฝั่ง client และต้องการแสดงชื่อหรือ avatar URL ของผู้ใช้จาก claims การถอดรหัสอย่างเดียวก็ใช้ได้ ฝั่ง server ต้องตรวจสอบเสมอ ผมเคยเห็น production system ที่ถอดรหัส JWT claims เพื่อตัดสินใจ access control โดยไม่ตรวจสอบลายเซ็น และนั่นเป็นช่องโหว่เปิดให้ ผู้โจมตีที่เข้าใจรูปแบบ JWT

JavaScript — jose.decodeJwt for decode-only scenarios
import * as jose from "jose";

// ถอดรหัสอย่างเดียว: ไม่ต้องใช้ secret ไม่ตรวจสอบ
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"

// ตรวจสอบการหมดอายุโดยไม่ตรวจสอบ (สำหรับแสดงผลฝั่ง client)
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
  console.log("Token has expired — redirect to login");
}

แสดงผล Terminal พร้อม Syntax Highlighting

เมื่อ debug JWT tokens ใน Node.js CLI tool หรือระหว่าง incident output ที่มีสีช่วยได้มาก ไลบรารี chalk ที่ใช้ร่วมกับ JSON.stringify ทำงานได้ดี ติดตั้งด้วย npm install chalk

Node.js — colorized JWT decode output
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)));

  // แสดงสถานะการหมดอายุ
  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..."
หมายเหตุ:output ที่มีสีใช้สำหรับ terminal เท่านั้น อย่าใช้ chalk เมื่อเขียน JWT claims ลงใน log file, API response หรือ database field ANSI escape codes จะปรากฏเป็น garbage ในบริบทที่ไม่ใช่ terminal

ประมวลผล JWT จาก Log File ขนาดใหญ่

API infrastructure สมัยใหม่ปล่อย access log แบบ structured ในรูปแบบ NDJSON — หนึ่ง JSON object ต่อบรรทัด โดยแต่ละบรรทัดมี request path, response status, latency และ header Authorization ที่ถอดรหัสแล้วหรือ raw ใน service ที่ยุ่ง ไฟล์เหล่านี้โตเร็ว: gateway ที่รับ 10,000 request ต่อนาทีสร้างรายการ log กว่า 14 ล้านรายการต่อวัน กรณีการใช้งานด้านความปลอดภัย และการปฏิบัติตามข้อกำหนดมักต้องการสแกนไฟล์เหล่านี้ภายหลัง — ระบุทุก request ที่ทำโดย service account ที่ถูก compromise (การวิเคราะห์หลัง incident) ยืนยันว่า token ของผู้ใช้เฉพาะหมดอายุก่อนช่วงเวลาการเข้าถึงข้อมูล (การ audit ตามกฎ) หรือดึง subject ทั้งหมดที่เข้าถึง endpoint ที่สำคัญระหว่าง maintenance window เนื่องจาก log file เดียว อาจมีขนาดหลาย GB การโหลดเข้าหน่วยความจำด้วย readFileSync จึงไม่เป็นไปได้ Node.js readline stream ประมวลผลไฟล์ทีละบรรทัดด้วย memory overhead คงที่ ทำให้สแกน log ขนาดใหญ่ได้บน developer laptop ทั่วไป

คุณจะไม่เจอปัญหา "ไฟล์ใหญ่เกินหน่วยความจำ" กับ JWT แต่ละตัว เพราะ token เดียวแทบไม่เกินสองสามกิโลไบต์ สถานการณ์ที่เกิดขึ้นคือการสแกน access log หรือ audit trail ขนาดใหญ่เพื่อหา JWT token ถอดรหัสทีละตัวและดึง claim เฉพาะ Node.js streams จัดการเรื่องนี้ได้โดยไม่ต้องโหลดไฟล์ทั้งหมด

Node.js — stream NDJSON log and decode embedded JWTs
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 {
      // ข้ามบรรทัดที่ผิดรูปแบบ
    }
  }

  console.log(`\nสแกน ${lineCount} บรรทัด พบ ${expiredCount} token ที่หมดอายุ`);
}

scanLogsForExpiredTokens("./logs/api-access-2026-03.ndjson");
Node.js — extract unique JWT subjects from log stream
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 {
        // ไม่ใช่ JWT ที่ถูกต้อง
      }
    }
  }

  console.log(`พบ ${subjects.size} subject ที่ไม่ซ้ำกัน:`);
  for (const sub of subjects) console.log(`  ${sub}`);
}

extractUniqueSubjects("./logs/gateway-2026-03.log");
หมายเหตุ:ใช้ streaming เมื่อ log file มีขนาดเกิน 50 MB การโหลดไฟล์ NDJSON ขนาด 500 MB ด้วย readFileSync จะตรึงหน่วยความจำและทำให้เกิด GC pauses แนวทาง readline ประมวลผลทีละบรรทัดด้วย memory usage คงที่

ข้อผิดพลาดที่พบบ่อย

ใช้ atob() โดยไม่มี TextDecoder สำหรับ non-ASCII claims

ปัญหา: atob() คืนค่า Latin-1 string อักขระ UTF-8 หลาย byte (emoji, CJK, อักขระที่มีสัญลักษณ์กำกับ) ถูกแยกข้ามตัวอักษรและออกมาเสียหาย

วิธีแก้: แปลง output ของ atob() ให้เป็น Uint8Array แล้วส่งผ่าน new TextDecoder('utf-8')

Before · JavaScript
After · JavaScript
// เสียหายกับ non-ASCII payload claims
const payload = JSON.parse(atob(token.split(".")[1]));
// display_name ปรากฏเป็น "ç°ä¸­å¤ªé\x83\x8E" แทนที่จะเป็น "田中太郎"
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 แสดง "田中太郎" ได้อย่างถูกต้อง
ลืมแทนที่อักขระ base64url เป็น base64

ปัญหา: atob() throw "InvalidCharacterError" เพราะ base64url ใช้ - และ _ แทน + และ /

วิธีแก้: แทนที่ - ด้วย + และ _ ด้วย / ก่อนเรียก atob() Node.js Buffer.from() กับ 'base64url' จัดการเรื่องนี้อัตโนมัติ

Before · JavaScript
After · JavaScript
// Throw: InvalidCharacterError: String contains an invalid character
const payload = atob(token.split(".")[1]);
const segment = token.split(".")[1];
const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
const payload = atob(base64); // ทำงานได้แล้ว
เชื่อถือ JWT claims ที่ถอดรหัสแล้วโดยไม่ตรวจสอบลายเซ็น

ปัญหา: ใครก็สามารถสร้าง JWT ที่มี payload ใดก็ได้ การถอดรหัสเพียงแค่อ่านข้อมูล — ไม่ได้พิสูจน์ว่า token ถูกออกโดย auth server ของคุณ

วิธีแก้: ฝั่ง server ต้องตรวจสอบลายเซ็นเสมอโดยใช้ jose.jwtVerify() หรือ jsonwebtoken.verify() การถอดรหัสอย่างเดียวยอมรับได้สำหรับแสดง user claims ฝั่ง client

Before · JavaScript
After · JavaScript
// อันตราย: ถอดรหัสแต่ไม่ตรวจสอบ
const claims = JSON.parse(atob(token.split(".")[1]));
if (claims.role === "admin") {
  grantAdminAccess(); // ผู้โจมตีสามารถปลอมแปลงได้
}
import * as jose from "jose";
const { payload } = await jose.jwtVerify(token, secretKey);
if (payload.role === "admin") {
  grantAdminAccess(); // ปลอดภัย — ลายเซ็นได้รับการตรวจสอบแล้ว
}
เปรียบเทียบ exp กับ Date.now() โดยไม่หารด้วย 1000

ปัญหา: JWT exp เป็นวินาทีนับจาก epoch แต่ Date.now() คืนค่ามิลลิวินาที การเปรียบเทียบจะบอกว่า token ถูกต้องเสมอเพราะ timestamp มิลลิวินาทีใหญ่กว่า 1000 เท่า

วิธีแก้: หาร Date.now() ด้วย 1000 และ floor ผลลัพธ์ก่อนเปรียบเทียบกับ exp

Before · JavaScript
After · JavaScript
// Bug: Date.now() เป็นมิลลิวินาที, exp เป็นวินาที
if (payload.exp > Date.now()) {
  console.log("Token is valid"); // เป็น true เสมอ — ผิด!
}
const nowSeconds = Math.floor(Date.now() / 1000);
if (payload.exp > nowSeconds) {
  console.log("Token is valid"); // การเปรียบเทียบที่ถูกต้อง
}

วิธีถอดรหัส JWT — เปรียบเทียบอย่างย่อ

วิธีการ
สภาพแวดล้อม
รองรับ UTF-8
ตรวจสอบลายเซ็น
กำหนด Type เอง
ต้องติดตั้ง
atob() + TextDecoder
Browser
N/A (อ่านอย่างเดียว)
ไม่
Buffer.from()
Node.js
N/A (อ่านอย่างเดียว)
ไม่
decodeURIComponent()
Browser (เดิม)
N/A (อ่านอย่างเดียว)
ไม่
jose
ทั้งสอง
✓ (JWS/JWE)
npm install
jsonwebtoken
Node.js
npm install
jwt-decode
ทั้งสอง
N/A
npm install

ใช้ atob() + TextDecoder สำหรับถอดรหัสฝั่งเบราว์เซอร์เมื่อต้องการแสดง claims ให้ผู้ใช้เท่านั้น ใช้ Buffer.from() ใน Node.js script และ CLI tool ใช้ jose เมื่อต้องการตรวจสอบลายเซ็น ซึ่งคือ auth middleware ทุกตัวฝั่ง server Package jwt-decode เป็นทางเลือกน้ำหนักเบาหากต้องการ API ฟังก์ชันเดียวสำหรับถอดรหัสอย่างเดียวในเบราว์เซอร์ สำหรับการตรวจสอบด้วยตาอย่างรวดเร็วโดยไม่ต้องเขียนโค้ด วาง token ลงใน เครื่องมือ JWT Decoder

คำถามที่พบบ่อย

จะถอดรหัส JWT token ใน JavaScript โดยไม่ใช้ไลบรารีได้อย่างไร?

แบ่ง token ด้วย "." แล้วเอาส่วนที่สอง (payload) มา ปรับการเข้ารหัส base64url โดยแทนที่ - ด้วย + และ _ ด้วย / เติม = ให้ครบ แล้วเรียก atob() ตามด้วย TextDecoder เพื่อได้ JSON string แบบ UTF-8 จากนั้นส่งผลลัพธ์ผ่าน JSON.parse() แล้วจะได้ claims object โดยไม่ต้องใช้ npm package แนวทางนี้ใช้ได้ในเบราว์เซอร์สมัยใหม่ทุกตัวและใน Node.js 18+ หากต้องการอ่าน header ด้วย ให้ใช้ขั้นตอนเดียวกันกับส่วนแรก โปรดทราบว่าวิธีนี้ให้ข้อมูลดิบโดยไม่ตรวจสอบลายเซ็น — ให้ถือว่าผลลัพธ์ใช้สำหรับแสดงผลเท่านั้น ยกเว้นจะตรวจสอบลายเซ็นฝั่ง server

JavaScript
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() เป็น browser API ที่ถอดรหัส Base64 มาตรฐานให้เป็น Latin-1 binary string โดยไม่รองรับ base64url โดยตรง จึงต้องแทนที่อักขระ - และ _ ก่อน ส่วน Buffer.from(segment, "base64url") เป็น Node.js API ที่รองรับ base64url natively และคืนค่า Buffer ที่เรียก .toString("utf-8") ได้เลย ใช้ atob() ในเบราว์เซอร์ และ Buffer.from() ใน Node.js อีกตัวเลือกหนึ่งคือ decodeURIComponent percent-encoding ซึ่งช้ากว่าและเป็นที่นิยมในอดีต แต่รูปแบบนั้นพึ่งพาฟังก์ชัน escape() ที่ถูก deprecated แล้วในบางโค้ดเก่า ควรหลีกเลี่ยงในโค้ดใหม่ สำหรับโค้ดที่รันได้ทั้งสองสภาพแวดล้อม ให้ตรวจสอบ typeof Buffer !== "undefined" แล้วแยกกรณีตามนั้น

JavaScript
// Browser
const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));

// Node.js
const json2 = Buffer.from(payload, "base64url").toString("utf-8");

เหตุใด atob() จึงทำงานผิดพลาดกับ JWT claims ที่ไม่ใช่ ASCII?

atob() คืนค่า Latin-1 string ที่แต่ละตัวอักษรแมปกับหนึ่ง byte ลำดับ UTF-8 แบบหลาย byte (emoji, อักขระ CJK, ตัวอักษรที่มีสัญลักษณ์กำกับเกิน Latin-1) จะถูกแยกข้ามหลายตัวอักษร ทำให้ผลลัพธ์เสียหาย วิธีแก้คือแปลง binary string ให้เป็น Uint8Array ก่อน แล้วส่งให้ new TextDecoder("utf-8").decode() API TextDecoder จะประกอบลำดับหลาย byte ได้อย่างถูกต้อง ปัญหานี้มักไม่ถูกพบในระหว่างพัฒนา เพราะ JWT payload ส่วนใหญ่มีเฉพาะ user ID, timestamp และชื่อ role ที่เป็น ASCII — bug จะปรากฏเมื่อ claim มีชื่อแสดงผลที่ไม่ใช่ ASCII หรือ string ที่แปลเป็นภาษาท้องถิ่น ควรใช้เส้นทาง TextDecoder เสมอในโค้ดใหม่แม้ payload ปัจจุบันจะเป็น ASCII อยู่ เพราะ claim อาจเปลี่ยนแปลงได้เมื่อแอปพลิเคชันพัฒนาขึ้น

JavaScript
// เสียหาย: atob คืนค่า Latin-1, อักขระหลาย byte เสียหาย
const broken = atob(base64); // "ð\x9F\x8E\x89" แทนที่จะเป็น emoji

// แก้ไข: แปลงเป็น byte array แล้วใช้ TextDecoder
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const fixed = new TextDecoder("utf-8").decode(bytes);

สามารถตรวจสอบลายเซ็น JWT ใน JavaScript ได้ไหม?

การถอดรหัสและการตรวจสอบเป็นคนละขั้นตอน การถอดรหัสเพียงแค่อ่าน payload ซึ่งไม่ได้เข้ารหัส การตรวจสอบจะตรวจลายเซ็นกับ secret (HMAC) หรือ public key (RSA/ECDSA) ไลบรารี jose รองรับทั้งสองอย่างในเบราว์เซอร์ผ่าน Web Crypto API และใน Node.js ส่วน jsonwebtoken ใช้ได้เฉพาะ Node.js เท่านั้น อย่าเชื่อ claims ที่ถอดรหัสแล้วโดยไม่ตรวจสอบลายเซ็นฝั่ง server ฝั่ง client ยอมรับได้ที่จะถอดรหัส JWT เพื่ออ่านชื่อผู้ใช้หรือเวลาหมดอายุ แต่การตัดสินใจเรื่องการเข้าถึง — ตรวจสอบว่าผู้ใช้มี role หรือสิทธิ์เฉพาะ — ต้องเกิดขึ้นในโค้ดฝั่ง server หลังการตรวจสอบแล้วเท่านั้น ผู้โจมตีที่เข้าใจรูปแบบ JWT สามารถสร้าง token ที่มี claims ใดก็ได้และการตรวจสอบฝั่ง client จะผ่าน

JavaScript
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 แล้วอ่าน claim exp ซึ่งเป็น Unix timestamp ในหน่วยวินาที เปรียบเทียบกับเวลาปัจจุบันโดยใช้ Math.floor(Date.now() / 1000) ถ้าเวลาปัจจุบันมากกว่า exp แสดงว่า token หมดอายุแล้ว จำไว้ว่า exp เป็นวินาทีนับจาก epoch ไม่ใช่มิลลิวินาที ดังนั้นต้องหาร Date.now() ด้วย 1000 ในทางปฏิบัติควรมี buffer สำหรับ clock skew — ตรวจสอบว่า token จะหมดอายุในอีก 30 วินาทีข้างหน้าแทนที่จะตรวจสอบเฉพาะอดีต จะป้องกันกรณีที่ token ยังใช้ได้เมื่อถอดรหัสแต่หมดอายุก่อน API call ถัดไปจะประมวลผล และควรจัดการกรณีที่ไม่มี exp ด้วย ซึ่งหมายความว่า token ไม่มีวันหมดอายุ

JavaScript
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 decode แบบ isomorphic ที่ทำงานได้ทั้งใน Node.js และเบราว์เซอร์ได้อย่างไร?

ตรวจสอบการมีอยู่ของ globalThis.Buffer ถ้ามีอยู่ แสดงว่าอยู่ใน Node.js และสามารถใช้ Buffer.from(segment, "base64url").toString("utf-8") ได้ ถ้าไม่มี แสดงว่าอยู่ในเบราว์เซอร์และควรใช้ atob() กับแนวทาง TextDecoder ห่อการตรวจสอบนี้ในฟังก์ชัน decodeBase64Url เดียวแล้วใช้ทุกที่ สิ่งนี้สำคัญที่สุดสำหรับ utility packages, design system components และโค้ดที่แชร์กันใน monorepo ที่ถูก import โดยทั้ง Next.js server component และ browser React component การเก็บการตรวจสอบสภาพแวดล้อมไว้ที่เดียวหมายความว่าต้องอัปเดตเพียงที่เดียวหาก runtime เปลี่ยน เช่น เมื่อ Deno รองรับ Buffer เต็มรูปแบบหรือ edge runtime ใหม่ต้องการเส้นทางโค้ดอื่น

JavaScript
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);
}

เครื่องมือที่เกี่ยวข้อง

มีให้ในภาษาอื่นด้วย:Python
MW
Marcus WebbJavaScript Performance Engineer

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.

SL
Sophie Laurentผู้ตรวจสอบทางเทคนิค

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.