JWTのpayloadをatobで読むと落ちる・文字化けする理由と直し方
JWT の中身を確認したくて、payload の部分を atob() に渡したところ例外が出た、あるいは日本語の名前が å±±ç°... のように化けた、という経験は多い。原因は 2 つで、どちらも JWT が使う Base64URL と、標準の Base64 との差から来る。
この記事では Node.js で両方を実際に起こし、直した関数を示す。なお、ここで扱うのはデコード(読むだけ)であり、署名の検証ではない。この点は最後に触れる。
仕様で押さえる点
JWT は RFC 7519、署名付きの形式 JWS は RFC 7515 で定義されている。ヘッダ、payload、署名の 3 つを . でつなぎ、各部分は Base64URL でエンコードする。Base64URL は RFC 4648 の URL 安全な表記で、標準の Base64 から次の点が変わる。
| 項目 | 標準 Base64 | Base64URL |
|---|---|---|
| 62 番目の文字 | + |
- |
| 63 番目の文字 | / |
_ |
末尾の = |
あり | JWS では付けない |
atob() は標準 Base64 用なので、- や _ を含む入力は受け付けない。また JWT の payload は JSON を UTF-8 のバイト列にしてからエンコードしたものなので、atob() が返す文字列をそのまま使うと、2 バイト以上の文字が壊れる。
実際に試す
前提は Node.js(ここでは Windows 11 上の v24.11.1)。atob と TextDecoder はブラウザにもあるので、同じコードがブラウザでも動く。次を decode-jwt.mjs として保存する。
1 | |
1. -と_を含むと atob は例外になる
payload に日本語と ?>>> を入れたトークンの一部を作り、素の atob に渡す。?>>> を入れたのは、Base64URL で - や _ が出る典型的なバイト列を意図的に作るためだ。
1 | |
1 | |
セグメントの中に _ と - があり、そのせいで InvalidCharacterError になった。- を +、_ を / に置き換えれば atob は通る。
2. パディングは足さなくても通る
= を補う処理は、よく見るコードに入っているが、実測では必須ではなかった。- と _ を置換したあとなら、= なしでも atob は復号した。
1 | |
1 | |
長さが 4 の倍数でないときの = 補完は、他の環境(古いライブラリや別言語)で必要になることがあるので、上の関数では補っている。ただし補う数を間違えると逆に例外になる点には注意が要る。長さを 4 で割った余りが 1 になる入力は、Base64 としてあり得ず、どのみち例外になる。
1 | |
1 | |
3. 日本語が化けるのは UTF-8 として読んでいないから
置換だけして atob の戻り値をそのまま使うと、例外は出ないが日本語が化ける。
1 | |
1 | |
atob はバイト列を 1 バイト 1 文字の文字列にして返すだけで、UTF-8 の解釈はしない。上の decodeSegment のように Uint8Array に直して TextDecoder に渡すと、"name":"山田太郎" が元通りになる。fatal: true を付けているので、UTF-8 として不正なバイト列(実測では 0xFF 0xFE)は黙って置換文字にならず、TypeError で止まる。
4. 通しで動かす
1 | |
1 | |
Node.js だけなら Buffer.from(seg, 'base64url').toString() の 1 行でも同じ結果になった。ブラウザでは Buffer が使えないので、上の関数を使う。
読めることと信頼できることは別
ここまでのコードは中身を読むだけで、署名は確かめていない。ペイロードは誰でも読めるし、書き換えた JWT も同じ手順でデコードできてしまう。権限判断に使うなら、署名、exp、iss、aud をライブラリで検証したうえで読むこと。検証の流れは、同じブログの SharePoint の JWT 検証チェーン の記事で扱っている。
手元でとりあえず中身を見たいだけなら、ブラウザ内で完結する JWT デコーダー(ハシトシステム) に貼ればよい。ただし本番の有効なトークンを共有端末や他人の画面に出すのは避けること。
参考
- RFC 7519 (JSON Web Token) / RFC 7515 (JSON Web Signature)
- RFC 4648 (Base64URL の定義)
- WHATWG Encoding Standard(
TextDecoderのfatalオプション) - Node.js ドキュメント Buffer(
base64urlエンコーディング)