コンテンツにスキップ

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を盗まれたら悪用される可能性がある。)

参考リンク