サーバーが 401 Unauthorized を返します。トークンの期限切れでしょうか、それとも権限が足りないのでしょうか。答えはトークンの中に書かれているのに、長くて意味のない文字列に見えるため、つい再ログインで済ませてしまいがちです。まずはその文字列を開いて読む方法から整理します。
JWT は暗号化ではなくエンコードである
JSON Web Token はドット(.)で繋いだ3つの断片です。header.payload.signature の順で、それぞれが base64url でエンコードされています。
よくある誤解がここで生まれます。base64url は暗号化ではなく単なるエンコードなので、秘密鍵がなくても誰でも元に戻して読めます。つまり payload に入ったユーザー ID や権限は、トークンを持つ人全員に見えます。パスワードや機微な情報を payload に入れてはいけない理由です。
payload に入るクレームを読む
payload の中の各項目をクレーム(claim)と呼びます。RFC 7519 が標準クレームを定義しているので、名前さえ分かれば意味がすぐ読めます。
| クレーム | 意味 |
|---|---|
iss | 発行者 (issuer) |
sub | トークンの主体 (subject、通常ユーザー ID) |
aud | 対象 (audience) |
exp | 有効期限 (expiration) |
iat | 発行時刻 (issued at) |
nbf | 有効開始時刻 (not before) |
jti | トークン固有 ID |
exp・iat・nbf の3つが時刻クレームで、デバッグで最もよく目にします。
有効期限の確認: exp は Unix エポックなので読みにくい
exp が有効期限なのですが、値は 1700003600 のような数値です。人が読むためではなく Unix エポック、つまり1970年からの経過秒数です。
そこでツールが変換を代行します。PiPi Worlds の JWT デコーダーにトークンを入れると、payload と一緒に時刻クレームが ISO 形式に展開されます。たとえば iat: 1700000000 は 2023-11-14T22:13:20Z、exp: 1700003600 は 2023-11-14T23:13:20Z に変換され、両者の差からこのトークンの有効期間が1時間だと分かります。有効期限が現在より過去なら期限切れバッジが付きます。401 が出たとき、このバッジひとつで「期限切れで更新が必要か」がすぐ判別できます。
デコードは検証ではない
最も大切な区別です。トークンをデコードして中身を読むことと、そのトークンが本物か検証することは別物です。
3つ目の断片である署名(signature)は、発行者が秘密鍵で作った値です。改ざんされていないか確認するには、同じ秘密鍵か公開鍵で署名を再計算して照合する必要があります(RFC 7515)。デコーダーは鍵を受け取らず保持もしないため、検証は行いません。したがってデコードして見える payload は「サーバーが署名を検証するまでは信頼できない値」として扱うべきです。payload の role が admin と書いてあっても、検証なしにその言葉を信じてはいけません。
その本番トークン、どこに貼っていますか
ここでセキュリティ事故がよく起きます。401 をデバッグしようと、本番環境のアクセストークンを適当なオンラインデコーダーに貼り付けるケースです。トークンそのものが認証手段なのに、入力をサーバーへ送信するサイトなら、その瞬間トークンが第三者に渡ります。
PiPi Worlds の JWT デコーダーは、デコードがすべてブラウザ内で完結します。トークンは送信・記録・保存されないため、本番トークンも安心して確認できます。payload が JSON なので構造をもっと見やすく展開したいなら JSON 整形ツールを、トークンを構成する base64url エンコード自体が気になるなら Base64 ツールを併せて使えます。