JWT ガイド

JavaScript で JWT をデコードするときに私が守る手順

JavaScript で JWT の header と payload を安全にデコードし、Base64URL、UTF-8、Bearer prefix、exp を正しく扱いながら署名検証と分離する方法を解説します。

著者: JWTDecoder.tools最終確認 2026-09-13

編集上の表現について:一人称の記述は JWTDecoder.tools の検証済みワークフローを示すもので、発行者・組織著者は JWTDecoder.tools です。

JWT → Base64URL → UTF-8 → JSON
JWT → Base64URL → UTF-8 → JSON · decode ≠ verify

JavaScript で JWT をデコードするとき、私は最初に デコードは inspection であって authentication ではない と決めてから実装します。JSON がきれいに parse できても、それだけでは token が正しい、信頼できる、authorization に使えるとは判断しません。

手早く見るだけなら JWT デコーダー を使います。アプリや debug script に同じ処理を入れるときは、次の順番にします。

私が使う 6 ステップ

  1. Bearer / Authorization: Bearer prefix を除去する。
  2. compact token の segment 数を確認する。
  3. Base64URL を正しく bytes に戻す。
  4. bytes を UTF-8 として decode する。
  5. JSON を明示的に parse し、error を分ける。
  6. exp などを読むが、signature verification とは分離する。

短い one-liner より少し長くても、この順番の方が broken input の原因を特定しやすくなります。

Step 1: input を normalize する

実際には raw JWT だけでなく次の形をコピーすることがあります。

Bearer eyJ...
Authorization: Bearer eyJ...

私は最初に token 本体へ揃えます。

function normalizeJwtInput(input) {
  return input
    .trim()
    .replace(/^authorization\s*:\s*bearer\s+/i, "")
    .replace(/^bearer\s+/i, "")
    .trim()
    .replace(/^['"]|['"]$/g, "");
}

この helper は意図的に単純にします。後続処理が「compact JWT だけが来る」という前提で動けるからです。

Step 2: segment 数を先に確認する

署名付き compact JWS は一般に:

header.payload.signature

の 3 セグメントです。JWE は通常 5 セグメントです。

function splitSignedJwt(input) {
  const token = normalizeJwtInput(input);
  const parts = token.split(".");

  if (parts.length === 5) {
    throw new Error("3-part signed JWT ではなく encrypted JWE の可能性があります");
  }
  if (parts.length !== 3) {
    throw new Error(`JWT は 3 セグメントを想定しています: ${parts.length}`);
  }

  return parts;
}

token.split('.')[1] だけ読むコードは、壊れた入力でも原因が見えにくいため私は避けます。

Step 3: Base64URL を普通の Base64 と混同しない

JWT segment は Base64URL です。- と _ を使い、padding = が省略されることがあります。

function base64UrlToBytes(segment) {
  const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64.padEnd(
    base64.length + ((4 - (base64.length % 4)) % 4),
    "="
  );

  const binary = atob(padded);
  return Uint8Array.from(binary, (char) => char.charCodeAt(0));
}

atob() は bytes を表す binary string を返すだけなので、ここで UTF-8 まで解決したとは考えません。

Step 4: UTF-8 として decode してから JSON.parse する

ASCII だけの payload なら雑な実装でも長く動いてしまいます。しかし名前や組織名などに非 ASCII 文字が入ると破綻します。

function decodeJwtJson(segment, label) {
  let text;
  try {
    const bytes = base64UrlToBytes(segment);
    text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
  } catch {
    throw new Error(`${label} は有効な Base64URL / UTF-8 ではありません`);
  }

  let value;
  try {
    value = JSON.parse(text);
  } catch {
    throw new Error(`${label} は有効な JSON ではありません`);
  }

  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`${label} は JSON object である必要があります`);
  }

  return value;
}

Base64URL/UTF-8 error と JSON error を分けると、原因のレイヤーを早く特定できます。

Step 5: 小さな decoder を組み立てる

function decodeJwt(input) {
  const token = normalizeJwtInput(input);
  const parts = token.split(".");

  if (parts.length !== 3) {
    throw new Error("3-part signed JWT を想定しています");
  }

  return {
    token,
    header: decodeJwtJson(parts[0], "JWT header"),
    payload: decodeJwtJson(parts[1], "JWT payload"),
    signature: parts[2]
  };
}

この時点で私は alg、kid、iss、aud、exp をログに出して調査できます。ただし結果は decoded であって verified ではありません。

Step 6: exp の秒とミリ秒を間違えない

JWT の exp、iat、nbf は NumericDate で、Unix epoch からの 秒 です。JavaScript の Date.now() はミリ秒です。

function getExpiration(payload) {
  if (typeof payload.exp !== "number") return { hasExpiration: false };

  const expiresAt = new Date(payload.exp * 1000);
  return {
    hasExpiration: true,
    expiresAt,
    expired: Date.now() >= payload.exp * 1000
  };
}

私は次の比較をしません。

Date.now() >= payload.exp // 単位が違う

時間条件だけなら JWT 有効期限チェック の方が iat / nbf も同時に見られます。

Node.js では Buffer を使うこともある

Node.js 専用なら inspection 用に Buffer を使うことがあります。

function decodeBase64UrlJson(segment) {
  const text = Buffer.from(segment, "base64url").toString("utf8");
  return JSON.parse(text);
}

function decodeJwtNode(token) {
  const parts = token.trim().split(".");
  if (parts.length !== 3) throw new Error("3-part JWT が必要です");

  return {
    header: decodeBase64UrlJson(parts[0]),
    payload: decodeBase64UrlJson(parts[1]),
    signature: parts[2]
  };
}

server で authentication に使う場合は、この decoder だけで済ませず、issuer、audience、algorithm policy を設定できる JOSE/JWT library で署名を検証します。

jwt-decode を使う場合も境界は同じ

client-side inspection だけなら jwt-decode を使うこともあります。

import { jwtDecode } from "jwt-decode";

const payload = jwtDecode(token);
const header = jwtDecode(token, { header: true });

この library 自体も「decode は validate ではない」と明確にしています。私は decoder package を server authentication gate として使いません。

JSON.parse(atob(token.split('.')[1])) を避ける理由

この one-liner は短いですが、私は再利用コードでは避けます。

  • payload segment が存在する前提になっている。
  • Base64URL と Base64 の違いを隠す。
  • padding を意識しない。
  • UTF-8 が壊れる可能性がある。
  • malformed input の error が分かりにくい。
  • alg / kid のある header を無視する。
  • signature 未検証という重要な境界を見失いやすい。

decode 後に必要な verification / validation

誰でも JSON を作って JWT らしい bytes を作れます。trust が必要なら JWT 署名検証 または JWKS Validator へ進みます。

signature が通っても、さらに:

  • exp / nbf
  • iss
  • aud
  • token type
  • scope / role

を application policy と照合します。JWT Validator と JWT デバッガー はその切り分けに使います。

browser で JWT を扱うときの私のルール

  • URL に JWT を入れない。
  • production bearer token を full log しない。
  • persistent storage に debug token を残さない。
  • synthetic / redacted token を優先する。
  • Base64URL decode のためだけに server へ token を送らない。

私の JavaScript JWT デコードチェックリスト

  1. Bearer prefix を除去する。
  2. segment 数を確認する。
  3. Base64URL を正しく bytes にする。
  4. UTF-8 として decode する。
  5. header / payload JSON を error handling 付きで parse する。
  6. alg、kid、iss、aud、time claims を読む。
  7. NumericDate を JavaScript Date にするとき * 1000 する。
  8. 結果を decoded と表示し、verified と言わない。
  9. trust が必要なら既知の key material で signature を検証する。
  10. acceptance が必要なら issuer、audience、time、token type、permissions を別に検証する。

JavaScript の decode 処理自体は難しくありません。実務で重要なのは、readable / cryptographically verified / application-acceptable の 3 状態を混ぜないことです。

出典・参考資料

関連ガイド