Skip to content
ブログに戻る
セキュリティ

JWT トークンをデコードする方法:開発者向け完全ガイド

JWT の三段構造と base64url を解説し、Node.js・Python・Go のデコード方法と無料オンライン JWT デコーダーを紹介。本番トークンも安全に検査できる。

12 分で読める

JWT トークンをデコードする方法:開発者向け完全ガイド

API から突然 401 Unauthorized が返ってきた。Authorization: Bearer eyJhbGciOi... ヘッダは一見問題ない。トークンの期限切れなのか、オーディエンスが間違っているのか、発行者が鍵をローテートしたのか。中身を実際に読まない限り判別できない。そして JWT をデコードするのに秘密鍵もライブラリもネットワーク接続も要らない。JWT はドットで連結された 3 つの base64url エンコード済みチャンクであり、デコードはただの機械作業だ。分割して、base64url で戻して、JSON.parse にかける。それだけの手順である。

本稿では JWT の構造を分解し、Node.js・Python・Go・ブラウザでのデコード方法を示す。合わせて、多くのチームが取り違える「デコードと検証の違い」と、実務で遭遇する失敗パターンも扱う。今すぐトークンを確認したいだけなら、無料の JWT デコーダーで済む。完全にブラウザ内で動くため、本番トークンがデバイスの外に出ることはない。

JWT とは何か(簡単な構造解説)

JSON Web Token(JWT)は RFC 7519 で定義された、コンパクトで URL セーフなクレデンシャルだ。ユーザーとトークン自体に関するクレーム(主張)を当事者間で運ぶ。ドットで連結された 3 つの base64url エンコード済み部分、すなわちヘッダー、ペイロード、シグネチャから成る。

実際のトークンを分解すると構造が見える。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9       ← header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0   ← payload
.
4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0       ← signature

ヘッダーはトークンの署名方式を示し、通常は { "alg": "HS256", "typ": "JWT" } のような内容になる。ペイロードはクレームを運ぶ。subexpiat といった登録済みクレームに加え、roletenant のようなカスタムクレームも入る。シグネチャはヘッダーとペイロードに対して計算された暗号学的な証明で、受信者が改ざんを検出するために使う。base64url は base64 の URL セーフ版だ。10 分で読める入門は初心者向け Base64 ガイドにある。

JWT は現代の認証の現場に常に登場する。OAuth 2.0 アクセストークン、OpenID Connect ID トークン、Auth0・Okta・Clerk・Supabase・Firebase が発行する API クレデンシャル、メッシュ内のマイクロサービス間で受け渡されるトークン。ここ 10 年、クレデンシャルのデフォルト形式になっている。

先に進む前に、一つだけ覚えてほしい原則がある。JWT はエンコードされているだけで、暗号化されてはいない。トークンを持つ者は誰でもクレームを読める。シグネチャが証明するのは出所であって、内容の秘匿ではない。この一点が以降の話すべてを規定する。ペイロードに何を入れて安全なのか、なぜデコードに秘密鍵が不要なのか、なぜサーバー側のシグネチャ検証が必須なのか、すべてここから決まる。

JWT デコードの仕組み(復号ではなく base64url)

JWT のデコードは暗号学的な操作ではない。機械的な 4 ステップだ。

  1. トークンを . で分割し、ちょうど 3 つのセグメントにする。
  2. 1 つ目のセグメントを base64url デコードし、JSON としてパースする。これがヘッダーだ。
  3. 2 つ目のセグメントを base64url デコードし、JSON としてパースする。これがペイロードだ。
  4. 3 つ目のセグメント(シグネチャ)は生のバイト列のまま残す。検証には鍵が必要になる。

アルゴリズムはこれで全部だ。ライブラリは必須ではない。base64 と JSON パーサーを持つ言語なら、5 行で JWT をデコードできる。ステップ 2 と 3 を手で追いたいなら、Base64 エンコーダー・デコーダーが使える。

base64url とは

base64url は、URL と HTTP ヘッダで安全に使えるよう通常の base64 に 3 つの調整を加えたものだ。+ の代わりに -/ の代わりに _ を使い、末尾の = パディングを省略する。この置換を元に戻さずに base64url を標準の base64 デコーダーに渡すと、文字化けかエラーが返る。パディングのエッジケースは応用 Base64 ガイドで詳しく扱っている。

標準 base64base64url
使用文字A-Z a-z 0-9 + /A-Z a-z 0-9 - _
パディング末尾の = が必須省略
URL セーフかいいえはい
PDw/Pz8+PDw_Pz8-

もう一点押さえておきたい。シグネチャはクライアント側で復号できない。デコードはエンコードされたバイト列から JSON への一方向変換だ。シグネチャ検証は別の操作で、HMAC 系アルゴリズムなら HMAC 秘密鍵、RS・PS・ES・EdDSA なら発行者の公開鍵が必要になる。

デコードに秘密鍵が不要な理由

ペイロードが暗号文ではなく、base64url と JSON だからだ。秘密鍵が必要になるのは、トークンが改ざんされていないことを証明したいとき、つまりシグネチャ検証のときだけである。ネットワーク経路上の誰でも、ログ行にトークンを持つ誰でも、ブラウザを持つ誰でも、ペイロードに入れたクレームをすべて読める。そのため JWT のペイロードに、パスワード・API キー・受信者がまだ知らない PII を入れてはならない。より広い脅威モデルはセキュリティベストプラクティスガイドにある。

3 クリックでオンライン JWT デコード — 無料 JWT デコーダー

今すぐ答えが欲しいときがある。このトークンは期限切れか、aud クレームは想定通りか、ヘッダーに alg:none と書かれていないか。そんなときは当サイトのオンライン JWT デコーダーが最短ルートだ。午前 2 時のインシデント対応のために作ってある。

  1. 貼り付け:入力欄にトークン全体を貼り付ける。ドットで区切られた 3 セグメントをすべて含めること。
  2. 読む:デコード済みのヘッダー、ペイロード、上部のステータスチップ(アルゴリズム、発行時刻、有効期限)を確認する。exp がすでに過去なら赤い Expired バッジが点灯する。
  3. コピー:必要なパネルをバグレポートや Slack スレッド、テストフィクスチャに貼り付ける。

本番トークンを貼っても安全な理由は次のとおりだ。

  • 100% ブラウザ動作。デコードはネイティブの atobJSON.parse を使う。ネットワークリクエストは一切発生しない。
  • ログなし、トラッキングなし、Cookie なし、サインアップなし。
  • ページ読み込み後はオフラインでも動く。

JWT デコーダーはアルゴリズム非依存だ。デコードに必要なのは base64url と JSON だけなので、あらゆる JWS バリアントを読める。HS256/384/512、RS256/384/512、PS256/384/512、ES256/384/512、EdDSA、そして alg:none まで対応する。アルゴリズムに依存するのは検証だけだが、検証は公開 Web ツールにやらせるべき処理ではない。これについては後述する。

ツールの結果をクロスチェックしたくて、セグメントを手で base64 デコードしたいときは、Base64 エンコーダー・デコーダーに各セグメントを base64url として渡せばよい。

コードで JWT をデコードする方法(Node.js、Python、Go、ブラウザ)

ミドルウェア、テスト、マイグレーションスクリプト、CLI ツールなど、インタラクティブなデバッグ以外の場面ではライブラリを使うことになる。ここでは遭遇率の高い 4 つの環境について、JWT をデコードする最小コードを、読み取り専用の経路と検証経路を並べて示す。すべてコピペで動き、コメントに書いた出力を生成する。

Node.js で JWT をデコードする(jsonwebtoken)

// npm install jsonwebtoken
const jwt = require('jsonwebtoken');

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' +
              '.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0' +
              '.4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0';

// デコードのみ — シグネチャは検証しない
const decoded = jwt.decode(token, { complete: true });
console.log(decoded.header);   // { alg: 'HS256', typ: 'JWT' }
console.log(decoded.payload);  // { sub: 'user_123', exp: 1999999999 }

// 検証 — 本番で使う経路
const secret = process.env.JWT_SECRET;
const verified = jwt.verify(token, secret, { algorithms: ['HS256'] });

verify には必ず明示的な algorithms 許可リストを渡すこと。これを省くと、攻撃者が RS256 トークンを HS256 にダウングレードし、公開鍵を HMAC 秘密鍵として署名に使える。古典的なアルゴリズム混同攻撃が成立してしまう。許可リストが防御線だ。

Python で JWT をデコードする(PyJWT)

# pip install PyJWT
import jwt

token = (
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
    ".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0"
    ".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"
)

# デコードのみ — 認証用途は危険、検査用途なら問題なし
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded)  # {'sub': 'user_123', 'exp': 1999999999}

# ペイロードに触れずヘッダーだけ取得
header = jwt.get_unverified_header(token)
print(header)   # {'alg': 'HS256', 'typ': 'JWT'}

# 検証 — 本番で使う経路
payload = jwt.decode(
    token,
    key="your-hs256-secret",
    algorithms=["HS256"],
    audience="api.example.com",
)

PyJWT は algorithms リストなしでの検証を拒否する。Node の例で警告したのと同じ混同攻撃を防ぐ、まっとうなデフォルトだ。

Go で JWT をデコードする(golang-jwt/jwt/v5)

// go get github.com/golang-jwt/jwt/v5
package main

import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

func main() {
    tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
        ".eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTk5OTk5OTk5OX0" +
        ".4NhxPjwoZxPNuxG-2C5ugGxaUsUJ0QyskAz7Ymz5Sg0"

    // デコードのみ
    parser := jwt.NewParser()
    claims := jwt.MapClaims{}
    _, _, err := parser.ParseUnverified(tokenString, claims)
    if err != nil {
        panic(err)
    }
    fmt.Println(claims["sub"], claims["exp"]) // user_123 1.999999999e+09

    // 検証
    secret := []byte("your-hs256-secret")
    token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
        }
        return secret, nil
    })
    fmt.Println(token.Valid, err)
}

keyFunc のクロージャがアルゴリズムファミリーを強制する場所だ。期待する方式でなければ、鍵を返す前に拒否する。

ブラウザで JWT をデコードする(依存ゼロ)

依存関係を一切持ち込みたくない場面もある。ちょっとしたデバッグパネル、ブラウザ拡張、現在のユーザーのロールを表示する小さな UI バッジなど。ネイティブのブラウザ API だけで十分対応できる。

function decodeJwt(token) {
  const [h, p] = token.split('.');
  const pad = (s) => s + '==='.slice((s.length + 3) % 4);
  const decodeSegment = (s) => {
    const b64 = pad(s).replace(/-/g, '+').replace(/_/g, '/');
    const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
    return JSON.parse(new TextDecoder().decode(bytes));
  };
  return { header: decodeSegment(h), payload: decodeSegment(p) };
}

const { header, payload } = decodeJwt(token);
console.log(header);   // { alg: 'HS256', typ: 'JWT' }
console.log(payload);  // { sub: 'user_123', exp: 1999999999 }

TextDecoder のステップは、非 ASCII ペイロード(表示名の絵文字、preferred_username のキリル文字など)を持つトークンで効く。素の atob はバイナリ文字列を返すので、マルチバイト UTF-8 では JSON.parse が壊れる。UI を除けば、オンライン JWT デコーダーがブラウザ内でローカルに実行しているのもほぼこれだ。

比較表

言語デコードのみ検証ライブラリ
Node.jsjwt.decode(token)jwt.verify(token, key, { algorithms: [...] })jsonwebtoken
Pythonjwt.decode(token, options={"verify_signature": False})jwt.decode(token, key, algorithms=[...])PyJWT
Goparser.ParseUnverified(token, claims)jwt.Parse(token, keyFunc)golang-jwt/jwt/v5
ブラウザatob + TextDecoder + JSON.parseバックエンドに委ねる

デコード対検証 — 決定的な違い

JWT のデコードはクレームを読み取る行為で、JWT の検証はそのクレームが改ざんされていないことを証明する行為だ。デコードは何も信用しない。検証が信用を生む。この線を引けるかどうかで、まともな認証実装になるか CVE を生むかが決まる。

デコード検証
秘密鍵・鍵が必要か不要必要
クライアント側で動かせるか安全に動く絶対にダメ
真正性を証明するかしないする
有効期限を検査するか任意する
ユースケースデバッグ、検査認証、認可

デコードしただけの(未検証の)JWT から認可判断を下してはならない。ミドルウェアでも、React フックでも、ゲートウェイの裏にあるはずのサーバーレス関数でもだ。デコード済みクレームはトークンが主張している内容を示すにすぎず、検証済みクレームこそ発行者が署名した内容を示す。有効なシグネチャなしに手作りしたトークンをサーバーに投げてくる攻撃者は、ペイロードに好きな値を詰められる。それを弾けるのはシグネチャ検査だけだ。

HMAC ファミリーについてもう一点。HS256 を使うなら、秘密鍵のエントロピーがすべてを決める。短くて推測可能な秘密鍵は、攻撃者が入手したトークンに対してオフラインで総当たりされる。そのあと攻撃者は自分でトークンを発行し、そのまま認証を通過してくる。最低でも 256 ビットの真の乱数を使うこと。なぜこの桁数が必要かの計算根拠はHMAC 秘密鍵強度ガイドで扱っている。

よく使われる JWT クレーム一覧

目にする JWT はほぼすべて、RFC 7519 の登録済みクレームのいずれかを使っている。短いリストを覚えておくと楽だ。

クレーム名称備考
issIssuer(発行者)https://auth.example.com誰がトークンを発行したか
subSubject(主体)user_123通常はユーザー ID
audAudience(対象者)api.example.comトークンの送り先 — サーバー側で一致必須
expExpiration(有効期限)1715003600Unix 秒、過去なら期限切れ
iatIssued At(発行時刻)1715000000トークン発行時の Unix 秒
nbfNot Before(有効開始)1715000060トークンが使える最も早い時刻
jtiJWT IDd1f8…トークンごとに一意、再生を防ぐ
kidKey ID(ヘッダー内)key-2025-01JWKS 内のどの鍵で署名したか

アプリケーション固有のクレームはこれらと並んで置かれる。rolescopeemailtenant_id など、アイデンティティプロバイダが発行するものだ。短く保つこと。すべてのバイトがリクエストごとに運ばれる。

iatexp を人が読める日時に変換したいときは、Unix タイムスタンプ変換ツールが使える。数値を貼り付けるとローカルタイムゾーンの日時が出て、時計のズレによるバグも一瞬で見抜ける。

トラブルシューティング — JWT がデコードできない理由

実務でよく遭遇する失敗パターンを頻度順に 5 つ挙げる。いずれも「症状 → 原因 → 対処」でまとめる。

  1. 「Invalid JWT format — expected three segments.」 ペイロードだけをコピーしてしまったか、シェルでトークンが改行されて先頭行しか拾えていない可能性が高い。対処:レンダリング済みターミナルからではなく、元のレスポンスボディから xxx.yyy.zzz の値を再コピーする。長い一行の値は、スクロールするターミナルよりブラウザの DevTools Network タブの方が生き残りやすい。
  2. 3 つではなく 5 つのセグメント。それは JWS ではなくJWE(暗号化された JWT)だ。形式は header.encryptedKey.iv.ciphertext.tag になる。デコーダーはヘッダーを読めるが、ペイロードは暗号文だ。対処:ペイロードのデコードには復号鍵が必要で、通常はデバッグツールではなく認証 SDK がサーバー側で処理する。
  3. 見た目は正しい JWT なのに base64url エラーが出る。コピー経路のどこか(Cookie、リダイレクト URL、キャプチャされたプロキシログ)でトークンが URL エンコードされている。文字列中に %2E%2B が素のまま見えるはずだ。対処:まずURL デコードし、その結果を JWT デコーダーに渡す。
  4. ペイロードで JSON パースエラー。ターミナルやチャットクライアントがソフトラップの改行を挿入したか、スクリプトが識別子をスマートクォートで囲んだ可能性がある。対処:生のレスポンスバイトを確認する(curl-o file.txt か、DevTools の Raw 表示)。空白を取り除いてもう一度貼り付ける。
  5. きれいにデコードできたのにバックエンドが拒否する。これはデコードではなく検証の問題だ。トークンは構造的には有効で、サーバーが検査している何か(シグネチャ、audexp、時計のズレ、kid ルックアップ)が失敗している。次のセクションに進もう。

パースエラーではないが、デコーダーを開いたついでに拾っておきたい 2 点がある。ヘッダーに algnone と書かれている場合(本番では敵対的なものとして扱う)と、exp が過去の場合(デコーダーはクレームを表示し続ける。これは正しい挙動で、当ツールは赤い Expired バッジで知らせる)だ。

デコードだけでは足りないとき — シグネチャ検証

デコードが示すのは「トークンはこう主張している」までだ。検証はこの主張を信用の判断に変える作業になる。シグネチャは発行者の秘密鍵または共有秘密鍵で計算された証明で、ヘッダーとペイロードを結びつける。1 バイトでも変われば検査は失敗する。この検査を省けば、エンドポイントに POST できる相手はペイロードを書き換え、署名を省いた自作「admin」トークンを送り込める。

alg:none は絶対に受け入れないこと

本番の検証は、どの言語・フレームワークでも、おおむね次のチェックリストのような形になる。欠けている項目はバグだと思っていい。

  • algorithms: ['RS256'](または使用するアルゴリズム)の許可リストを明示的に渡す。これがアルゴリズム混同攻撃を防ぐ。
  • aud が自サービスの識別子と、iss が想定する発行者 URL と一致することを確認する。
  • exp を現在時刻と比較し、許容する時計のズレは最大 60 秒以内にする。
  • 鍵ローテーション時は、kid を使って JWKS エンドポイントから公開鍵を引く。単一の鍵を永遠にハードコードしない。
  • exp を短く(日ではなく分)保ち、必要なら高価値トークン用に jti 拒否リストを運用することで、失効を実効的にする。

メジャーな JWT ライブラリは、これらを一つの呼び出しのオプションとして公開している。検証コードにこれらが設定されていなければ、それはデフォルトに頼っているということだ。そのデフォルトが過去に何度もバグの温床になってきた。完全な脅威モデルはセキュリティベストプラクティスガイドにある。

FAQ

秘密鍵なしで JWT をデコードできるか

できる。ヘッダーとペイロードは base64url でエンコードされているだけで、暗号化されていない。トークンを持つ者なら誰でもクレームを読める。秘密鍵や公開鍵はシグネチャを検証するときだけ必要になる。これは設計上の意図で、ペイロードは受信者が認可判断を下せるよう、読み取り可能になっている。

本番 JWT をオンラインデコーダーに貼り付けても安全か

デコーダーがブラウザ内で動作し、トークンをアップロードしない場合に限り安全だ。当サイトのJWT デコーダーはネイティブの atobJSON.parse でローカル解析する。どのサーバーにも何も送らない。トークンを API に POST するリモートデバッガーは、クレデンシャル漏洩と同じ扱いにするべきだ。

JWT のデコードと検証の違いは何か

デコードはクレームを読むだけで、鍵は不要、何も証明しない。検証は発行者の鍵に対してシグネチャを照合し、トークンが改ざんされていないことを確認する。デコードしただけで未検証のトークンから認証判断を下してはならない。

JWT が切り詰められて見える。正しい形式とは

有効な JWT はドットで区切られたちょうど 3 つの base64url セグメントを持つ(header.payload.signature)。5 セグメントなら JWS ではなく暗号化 JWT(JWE)だ。ドットがないなら、改行で折り返されたターミナル行から 1 セグメントしかコピーできていない。

期限切れトークンもデコーダーが表示するのはなぜか

デコーダーは有効性に関わらずクレームを読む。これは拒否の原因を調査するためだ。期限切れトークンを弾くのは検証ツールだけである。当ツールは exp をローカル時計と比較して Expired バッジを表示するので、Unix タイムスタンプを睨まずに問題を発見できる。

どのアルゴリズムのトークンをデコードできるか

全部だ。デコードに必要なのは base64url と JSON パースだけで、アルゴリズム非依存である。HS256/384/512、RS256/384/512、PS256/384/512、ES256/384/512、EdDSA、そして alg:none まで含む。アルゴリズムに依存するのは検証だけだ。

Node.js では jwt-decodejsonwebtoken のどちらを使うべきか

フロントエンドでペイロードを読むだけなら jwt-decode を使う。たとえばアクセストークンからユーザー名を表示するような用途だ。バックエンドでは jsonwebtoken を使う。署名鍵を保持して jwt.verify を実行できるのはバックエンドだけだからだ。クライアントで検証してはいけない。

まとめ

JWT のデコードは「暗号学的トークン」という響きほど大げさなものではない。次の 5 点を頭に入れておけば、不透明な eyJhbGciOi… 文字列を前にして手が止まることはない。

  • デコードは base64url と JSON パースだけで済む。秘密鍵は不要。
  • JWT はヘッダー、ペイロード、シグネチャの 3 部からなり、ドットで連結される。
  • デコードは真正性を一切証明しない。必ずサーバー側で発行者の鍵を使って検証する。
  • alg:none を拒否し、verify には常に明示的なアルゴリズム許可リストを渡す。
  • パスワード、秘密鍵、機微な PII をペイロードに保存しない。トークンを持つ者なら誰でも読める。

オンコールデバッグ用に無料 JWT デコーダーをブックマークしておくとよい。トークンを貼り付け、クレームを読み、期限切れを 1 秒で見つけられる。しかもトークンはブラウザから一歩も外に出ない。