JavaScript で JWT をデコードするとき、私は最初に デコードは inspection であって authentication ではない と決めてから実装します。JSON がきれいに parse できても、それだけでは token が正しい、信頼できる、authorization に使えるとは判断しません。
手早く見るだけなら JWT デコーダー を使います。アプリや debug script に同じ処理を入れるときは、次の順番にします。
私が使う 6 ステップ
Bearer/Authorization: Bearerprefix を除去する。- compact token の segment 数を確認する。
- Base64URL を正しく bytes に戻す。
- bytes を UTF-8 として decode する。
- JSON を明示的に parse し、error を分ける。
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/nbfissaud- 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 デコードチェックリスト
- Bearer prefix を除去する。
- segment 数を確認する。
- Base64URL を正しく bytes にする。
- UTF-8 として decode する。
- header / payload JSON を error handling 付きで parse する。
alg、kid、iss、aud、time claims を読む。- NumericDate を JavaScript Date にするとき
* 1000する。 - 結果を decoded と表示し、verified と言わない。
- trust が必要なら既知の key material で signature を検証する。
- acceptance が必要なら issuer、audience、time、token type、permissions を別に検証する。
JavaScript の decode 処理自体は難しくありません。実務で重要なのは、readable / cryptographically verified / application-acceptable の 3 状態を混ぜないことです。