調べて分かる道具箱

JWT の中身を見る

貼ったトークンはこの画面の中だけで読みます。 送信も保存もしません。URL にも入れないので、タブを閉じれば何も残りません。 ブラウザの開発者ツールで「ネットワーク」を開いたまま貼ると、 通信が1件も増えないことが見えます。

ピリオドで3つに分かれています
1つ目・ヘッダ36文字 = 27バイト
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2つ目・ペイロード132文字 = 99バイト
eyJpc3MiOiJodHRwczovL3BvdGFtby5uZXQiLCJzdWIiOiJrYWJhLTAwMDEiLCJuYW1lIjoi44Od44K_44OiIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9
3つ目・署名43文字 = 32バイト
7XIyLWTqO8O2ZEP2iwilNCR0oq8mKre9HY0o8kKg_oA
ヘッダ(1つ目)は「どうやって署名したか」
alg署名の作り方。ヘッダで唯一の必須項目(RFC 7515)
HS256
typこのかたまりが何か。JWT なら JWT と書く(RFC 7515)
JWT

HS256 は共通鍵(HMAC・SHA-256)の方式です。送る側と受け取る側が同じ鍵を持つ。

{
  "alg": "HS256",
  "typ": "JWT"
}
ペイロード(2つ目)は「中身」
iss発行した人(Issuer)(RFC 7519)
https://potamo.net
sub誰についてのトークンか(Subject)(RFC 7519)
kaba-0001
name表示に使う名前(OpenID Connect Core 1.0)
ポタモ
iat発行した時刻(秒)(RFC 7519)
17672256002026-01-01 00:00:00 UTC
exp期限。この時刻以降は受け取ってはいけない(秒)(RFC 7519)
17672292002026-01-01 01:00:00 UTC
{
  "iss": "https://potamo.net",
  "sub": "kaba-0001",
  "name": "ポタモ",
  "iat": 1767225600,
  "exp": 1767229200
}
署名(3つ目)
署名そのものハッシュや楕円曲線の計算結果。
7XIyLWTqO8O2ZEP2iwilNCR0oq8mKre9HY0o8kKg_oA
長さほどいたあとのバイト数
32バイト
署名が計算された相手デコードしたあとの JSON ではなく、この文字列そのもの(RFC 7515 の JWS Signing Input)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3BvdGFtby5uZXQiLCJzdWIiOiJrYWJhLTAwMDEiLCJuYW1lIjoi44Od44K_44OiIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDB9

このページは署名を確かめません。確かめるには鍵が要ります。鍵を受け取らない代わりに、「正しいトークンです」とは決して言いません。 ここに出ているのは「そう書いてある」という事実だけです。

よくある質問

JWT の中身は、誰でも見られるのですか?

見られます。JWT は暗号化されていません。ヘッダとペイロードは base64url という書き写しの方式で書かれているだけなので、鍵を持っていない人でも元の JSON に戻せます。このページが鍵を1つも受け取らずに中身を出せているのが、その証拠です。読めないようにしたい場合は、JWE という暗号化された別の形を使います。

JWT に秘密の情報を入れてもいいですか?

入れてはいけません。パスワード、秘密鍵、クレジットカード番号、マイナンバーなどを入れると、トークンを受け取った人だけでなく、通信ログや履歴に残ったトークンを見た人にも読まれます。RFC 7519 の12節は「プライバシーに関わる情報を JWT に入れないことが、いちばん簡単な対処である」と書いています。

このページで署名を検証できますか?

できません。意図してその機能を作っていません。署名を確かめるには鍵が必要で、鍵を持たないまま「検証しました」と表示するのは危険だからです。RFC 8725 の2.1節は、alg を none に書き換えられたのに署名を見ずに通してしまったライブラリと、RS256 を HS256 にすり替えて RSA の公開鍵を共通鍵として使わせた攻撃(CVE-2015-9235)を挙げています。このページは alg が none だったとき、それ自体を警告として表示します。

exp の数字は何ですか?日時に直せますか?

1970年1月1日0時0分0秒(UTC)からの秒数です。RFC 7519 では NumericDate と呼び、exp・nbf・iat・auth_time がこの形です。JavaScript の Date はミリ秒で受け取るので、1000を掛けてから渡します。掛け忘れると1970年付近の日時になり、しかも「期限切れ」ともっともらしく表示されるので気づけません。このページは秒として読んだうえで、UTC と端末の時間帯の両方を出します。

「3つに分かれていません」と出ました。何が起きていますか?

JWT はヘッダ・ペイロード・署名をピリオドでつないだ形なので、ピリオドが2つ(区画が3つ)あります。1つしかなければ署名が抜けているか途中で切れています。ピリオドが4つで区画が5つのときは JWE という暗号化された形で、こちらは鍵が無ければ中身を読めません。RFC 7516 の9節が、この区画の数を JWS と JWE の見分け方として挙げています。

base64 と base64url は同じものですか?

違います。base64url は、62番目と63番目の文字が + と / ではなく - と _ で、末尾の = を付けません。URL やファイル名の中で + / = が別の意味を持ってしまうのを避けるためです。RFC 4648 の5節は「base64 と同じものとみなすべきではない」と明記しています。そのまま atob のような base64 用の関数に渡すと、- と _ のところで失敗します。

貼り付けたトークンは、どこかに送信されますか?

送信しません。保存もしません。処理はすべてブラウザの中で終わり、トークンを URL のクエリに入れることも、localStorage に書くこともありません。クリップボードを自動で読むこともしないので、貼るかどうかは利用者が決められます。タブを閉じれば何も残りません。

貼り付けると、ヘッダとペイロードを項目の意味つきで並べます。exp や iat は秒なので、日時に直して出します。

JWT は暗号化されていません。だから誰でも中身を読めます

いま、このページは鍵を1つも受け取っていません。 それなのに中身が出ました。これが JWT のいちばん大事な性質です。JWT は暗号ではありません。ヘッダとペイロードは、バイトの並びを英数字だけで書き写すbase64url という方式で書かれているだけで、 誰でも元の JSON に戻せます。

見た目が意味不明な英数字の列なので、暗号のように見えます。 そこを取り違えて、パスワードや秘密の設定値をペイロードに入れてしまう間違いが起きます。入れた瞬間に、それは秘密ではなくなります。トークンは通信のログやブラウザの履歴にも残るので、 受け取った相手だけが読むとは限りません。

RFC 7519(新しいタブで開きます)の12節は、対処として3つを挙げたうえで「プライバシーに関わる情報を JWT から省くことが、いちばん簡単なやり方である」と書いています。読めないようにしたいときは、JWE という本当に暗号化された別の形を使います。

署名は「書き換えられていない」ことの印で、「読めない」ことの印ではありません

では3つ目の署名は何のためにあるのか。中身が途中で書き換えられていないことを確かめるためです。 隠すためのものではありません。ここを取り違えると、 「署名がついているから中身は見えない」という逆の理解になります。

面白いのは署名が何に対して計算されているかです。RFC 7515(新しいタブで開きます)は、署名の相手を「ヘッダの base64url とピリオドとペイロードの base64url をつないだ文字列」と定めています。つまり元の JSON ではなく、書き写したあとの文字列そのもの。 だから同じ内容でも、空白の入れ方や項目の順番が違えば、署名も別の値になります。 上の欄にも、その「署名が計算された相手」を出しています。

このページは署名を確かめません。鍵を持たないからです

署名を確かめるには鍵が要ります。このページは鍵を受け取りません。 だから「正しいトークンです」とは決して表示しません。 これは機能が足りないのではなく、意図してそうしています。

RFC 8725(新しいタブで開きます)(JWT の安全な使い方をまとめた文書)の2.1節は、実際に起きた攻撃を2つ挙げています。 1つは、攻撃者がalg を none に書き換えると、 その値をそのまま信じて署名を見ずに「検証した」ことにしてしまうライブラリがあったというもの。もう1つは、RS256(公開鍵の方式)を HS256(共通鍵の方式)にすり替えると、誰でも手に入る RSA の公開鍵を共通鍵として署名を確かめてしまうというもので、CVE-2015-9235 として記録されています。

どちらも鍵を確かめないまま「検証した」ことにしてしまった事故です。 鍵を持たないこのページが検証を名乗れば、同じ間違いを教えることになります。 だからalg が none のトークンを「確認できました」とは出さず、 それ自体を警告として出します。 署名なしのトークンはRFC 7519 の6節(新しいタブで開きます)が Unsecured JWT として認めている形ですが、誰でも中身を書き換えられます。

base64url は base64 ではありません

違いは2つだけです。62番目と63番目の文字が + と / ではなく - と _であること、そして末尾の = を付けないこと。 URL の中で + は空白の意味に、/ は区切りの意味に、= は値の代入の意味に なってしまうので、URL に入れても壊れない字だけを選んだ結果です。

RFC 4648 の5節(新しいタブで開きます)は、この方式について「base64 と同じものとみなすべきではないし、単に base64 と呼ぶべきでもない」とわざわざ書いています。実際、base64 用の関数にそのまま渡すと- と _ のところで失敗します。 JWT の中身が読めないという話の多くが、ここでつまずいています。 上の見本のペイロードにも _ が入っているので、探してみてください。

exp や iat は「秒」です。ミリ秒ではありません

期限(exp)・発行時刻(iat)・使い始められる時刻(nbf)は、1970年1月1日0時0分0秒(UTC)からの秒数で書きます。RFC 7519 はこれをNumericDate と呼んでいます。Unix時間と同じ数え方です。

気をつけるのはここです。JavaScript の Date はミリ秒で受け取るので、1000を掛けてから渡します。掛け忘れると 1767229200 は1970年1月21日になります。しかも画面には「期限切れ」ともっともらしく出るので、 表示を見ても間違いだと気づけません。逆に、秒の欄にミリ秒を入れてしまうと西暦57971年の期限になり、 こちらは「いつまでも切れないトークン」になります。 このページは桁が多すぎる値を見つけたら、そのことも書きます。

ピリオドで5つに分かれていたら、それは JWE です

JWT には署名だけの形(JWS)と、暗号化された形(JWE)があります。 見分け方はとても簡単で、RFC 7516 の9節(新しいタブで開きます)が「JWS は区画が3つ、JWE は区画が5つ」と書いています。

5つに分かれていたら、それは本当に暗号化されているので、 鍵が無ければ中身を読めません。このページも開けません。開けないことが正しい動きです。 3つに分かれているのに読めないときは、途中で切れているか、 トークン以外の文字が混ざっています。

貼ったトークンは、どこにも送りませんし、残しません

JWT は生きた合鍵です。人はうっかり本番のトークンを貼ります。 だからこのページでは、送らないことに加えて残さないことも決めています。 トークンを URL のクエリに入れません(入れると履歴やブックマークに残ります)。 localStorage にも書きません(共有の端末では、次に使う人に見えます)。 クリップボードを勝手に読むこともしません。

確かめる方法があります。ブラウザの開発者ツールで「ネットワーク」の欄を開いたまま トークンを貼ってください。新しい通信が1件も増えません。 機内モードにしても同じように動きます。 このサイトはサーバー側に処理を持たない造りなので、送ろうとしても送る先がありません。

出どころ: 3つの区画と base64url はRFC 7515(新しいタブで開きます)、claim と NumericDate はRFC 7519(新しいタブで開きます)、危ないところはRFC 8725(新しいタブで開きます)、字の決まりはRFC 4648(新しいタブで開きます)から取っています。項目の意味には、その名前を決めている文書の番号を並べて出しています。