コンテンツにスキップ

JWT

JWT(JSON Web Token)はトークンの一種である。トークンベース認証で使える。

JSON Web Token Introduction - jwt.io

まとめ

JWT では以下のことができる。

  • JWT を使うことで、サーバはクライアントを本人だと確認することができる。
  • JWT を使うことで、サーバはトークンが改ざんされていないことを確認できる。

JWT では以下のことができない。

  • JWT を使っても、クライアントが内容を読み取れないようにすることはできない。クライアントは簡単に内容を読み取ることができるし、書き換えることもできる。(ただし上記の通りクライアントが書き換えるとサーバーは書き換えられたことを確認できる)
  • JWT が盗まれたとき、サーバはクライアントが本人でないことを確認することができない。(本人だと思い込んでしまう)

JWT の中身

JWT はヘッダ(Base64Url)、ペイロード(Base64Url)、署名(Base64Url)で構成されている。

それぞれは Base64Url でエンコードされたあと.で結合されており、ヘッダ(Base64Url).ペイロード(Base64Url).署名(Base64Url)という具合である。

JWT の実例を以下に挙げる。

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJhNzM3YzNhZS02MDc1LTQzNzQtYWNiZS03MDc5MTAzZmNkZTYiLCJpYXQiOjE3MjIyMTQzNjcsImV4cCI6MTcyMjIxNDk2N30.N2sCZDnn2_1yUmbawqBjOW6VS9hXFrodMvr1gDMmvu4

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9がヘッダ、eyJ1c2VySWQiOiJhNzM3YzNhZS02MDc1LTQzNzQtYWNiZS03MDc5MTAzZmNkZTYiLCJpYXQiOjE3MjIyMTQzNjcsImV4cCI6MTcyMjIxNDk2N30がペイロード、N2sCZDnn2_1yUmbawqBjOW6VS9hXFrodMvr1gDMmvu4が署名という具合である。

ヘッダ

ヘッダの内容を確認してみる。そのままでは読めないのでデコードしてみる。

atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")で以下が得られる。

1
2
3
4
{
  "alg": "HS256", // 署名生成のアルゴリズム。"HS256"はHMAC SHA256を使った共通鍵暗号方式を示す。
  "typ": "JWT" // トークンのタイプ。"JWT"固定だと思えばいいと思う。
}

ヘッダを見ればこの謎の文字列が JWT であることと、HMAC SHA256 というアルゴリズムで署名が生成されていることがわかる。

ペイロード

atob("eyJ1c2VySWQiOiJhNzM3YzNhZS02MDc1LTQzNzQtYWNiZS03MDc5MTAzZmNkZTYiLCJpYXQiOjE3MjIyMTQzNjcsImV4cCI6MTcyMjIxNDk2N30")で以下が得られる。

1
2
3
4
5
{
  "userId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // パブリッククレーム 例ではUUIDを入れている。
  "iat": 1722214367, // 登録済みクレーム(JWTが発行された時刻)
  "exp": 1722214967 // 登録済みクレーム(JWTの有効期限)
}

ペイロードは登録済みクレーム、パブリッククレーム、プライベートクレームが含まれる。

ペイロードは次の署名によって改ざんから保護されているが、ペイロードはここまででわかるようにクライアント側で簡単に読むことができるため、パスワードやクレジットカード番号など秘密の情報を入れてはいけない。

登録済みクレーム

登録済みクレームは、みんなが認証で使うであろうものを仕様として用意したものである。iss(発行者)、exp(有効期限)、sub(件名)、aud(対象者)などがある。

登録済みクレームの一覧はRFC 7519 - JSON Web Token (JWT)で確認できる。

パブリッククレーム

パブリッククレームはユーザーが自由に定義できるクレームである。

プライベートクレーム

プライベートクレームは公式サイトの説明を読んでもよくわからない。用途も不明。

署名

署名はヘッダ(Base64Url)とペイロード(Base64Url)をピリオドで繋いだものをヘッダに指定したアルゴリズムと秘密鍵で署名する。さらに Base64Url でエンコードする。

以下はイメージである。(これはイメージであってこんなライブラリは存在しない)

1
2
3
encodedSign = base64UrlEncode(
  HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
);

クライアントとサーバの JWT 読み書き可不可

中身 クライアントの読み取り クライアントの書き込み サーバの読み取り サーバの書き込み
ヘッダ ✅️(*1) ❌️(*3) ✅️(*1) ✅️(*6)
ペイロード ✅️(*1) ❌️(*3) ✅️(*1) ✅️(*6)
署名 ❌️(*2) ❌️(*4) ✅️(*5) ✅️(*6)
  1. Base64Url でデコードするだけで容易に読むことができる。
  2. 署名はサーバの秘密鍵によって暗号化されているので、クライアントは署名を読み取ることができない。
  3. 署名の改ざんをしたらサーバにバレるので、改ざん不可能である。(サーバが適切に実装されていればの話だが)
  4. 署名を読むことができないので改ざんできない。
  5. 秘密鍵によって複合し、読むことができる。
  6. JWT の生成はサーバの役割である。

その他コメント

JWT のペイロードはサーバもクライアントも読むことができる。JWT は改ざんを検知する仕組みである。

クライアントは実質読み取りだけできるようになっている。

JWT において、クライアントは鍵を使わない。サーバが秘密鍵を使うだけである。そのため JWT は公開鍵暗号方式ではない。サーバーで完結する共通鍵暗号方式といえる。

JWT の保存方法

  • JWT を LocalStorage に保存する
    • 👍 クライアントの JavaScript がペイロードを読める。
    • 👎 クライアントの JavaScript から読めるので XSS 攻撃に弱い。
      • 以下の手続きを行うことで若干軽減できる。しかし XSS 攻撃への隙は無くならない。
        • アクセストークンとしての JWT の有効期限を短くする(例: アクセストークンの有効期限 5 分)
        • リフレッシュトークンを HttpOnly の Cookie に保存するようにする(例: リフレッシュトークンの有効期限 1 時間)
        • サーバーがリクエスト受付時にアクセストークンを確認し、有効期限が切れていたらリフレッシュトークンを参照し、リフレッシュトークンが生きていたら新しいアクセストークンを発行しクライアントに返す
  • JWT を Cookie に保存する
    • 長短は LocalStorage と変わりない。
  • JWT を HttpOnly の Cookie に保存する
    • 恐らく JWT の保存方法としては最も安全。
    • トークンを保管するのはクライアントに変わりないため、バックエンドはステートレスである。
    • 👍 クライアントの JavaScript から JWT を読めなくなるので、XSS 攻撃に強い。
    • 👎 クライアントの JavaScript がペイロードを読めない。(クライアントがペイロードを読みたい場合、サーバーにデコードさせることになる)
      • クライアントは自身が認証済みかどうかもわからない。
  • JWT を SessionStorage に保存する
    • 👍 クライアントの JavaScript がペイロードを読める。
    • 👍 SessionStorage の内容はタブを閉じると削除されるため、LocalStorage よりは JWT を盗まれる機会が減る。(盗まれる危険性が無くなるわけではない。SessionStorage から JWT が消えても JWT は JWT の有効期限の範囲で生きるため、JWT を盗まれたら悪用される可能性がある。)

参考リンク