JWT(JSON Web Token)とは
認証情報のやり取りに用いられる、JSONを用いた情報交換の規格。
ログイン成功時にトークンを発行し、その後のリクエストでこのトークンを Authorization ヘッダに付けて送信することで、サーバ側でログインユーザの識別・検証ができる。
サーバ側でセッション情報を保持する必要がなく、複数サーバに負荷分散しているような環境でも使用可能。
JWTの構成
ヘッダ、ペイロード、署名 の3つの部分で構成される。
それぞれが Base64 でエンコードされ、ドットで連結されて生成される。
ヘッダやペイロードをデコードするとJSONになっている。
header.payload.signature
ヘッダ
トークンのタイプ(JWT)と使用した署名アルゴリズム(HS256、RS256など)
{
"alg": "RS256",
"typ": "JWT"
}
ペイロード
ユーザIDや権限、有効期限など、実際のデータ(クレーム)
標準のフィールドに加え、自由にカスタムフィールドを追加できる
例)loggedInAsはカスタムフィールド
{
"loggedInAs" : "admin",
"sub": "1234567890",
"iat" : 1722779638
}
標準フィールド例:
jti(JWT ID):発行者ごとトークンごとに一意な識別子
sub(Subject):ユーザ ID など、認証したい対象の識別子を入れる(ユーザの特定に使用する)
exp(Expiration Time):トークンの有効期限(UNIX秒)
署名
ヘッダ、ペイロード、鍵をヘッダに記載した署名アルゴリズムに流して生成されたもの
トークンの改ざん検知に使用される
アクセストークンとリフレッシュトークン
アクセストークンだけでも機能するが、以下の理由からリフレッシュトークンを含めた2トークン制が推奨される。
アクセストークンは短命なので盗まれても被害を限定でき、リフレッシュトークンは送信頻度が低く厳重に保管されるため漏洩リスクが低い。両方が漏洩しない限り、長期的な不正アクセスは困難となる。
アクセストークン
APIリソースへのアクセス権を証明するトークン
- 有効期限を短く設定する(通常15分〜1時間程度)
- 各APIリクエストのヘッダに含めて送信する
- ユーザの識別情報や権限情報を含む
- 短命なので、漏洩時の被害を最小限に抑えられる
リフレッシュトークン
新しいアクセストークンを取得するためのトークン
これでアクセストークンを更新し、ユーザが頻繁に再ログインしなくてもいいようにする
- 有効期限を長く設定する(数日〜数週間、数ヶ月など)
- 安全な場所に保存する(後述)
- アクセストークンの有効期限が切れたときのみ使用
- 一度使用したら新しいものに更新する(ローテーションする)とより安全
→ 万が一Cookieが盗まれたとしても、一度使われた瞬間に盗んだ側のトークンも無効になる(あるいは検知できる)
セキュリティ上の特徴
中身は見られる
暗号化はされていないので、Base64 でデコードすればヘッダ・ペイロードの中身はクライアントが確認できてしまう。
ペイロードには機密情報は含めるべきではなく、ユーザの特定に最小限必要な情報だけを格納するようにする。
改ざんの検知
アクセストークンのペイロードのuser_id等を別人のものに改ざんし、別ユーザとしてリクエストを送れてしまう。
ただし、署名は正しいヘッダ・ペイロードを元に作られているため、バックエンドのトークン検証時に改ざんを検知し不正なリクエストを弾くことができる。
推奨構成
アクセストークン
→ フロントエンドのメモリのみに保持
永続化されずXSSにも強い。ページリロードで失われるが、リフレッシュトークンが残れば再ログインは不要。
リフレッシュトークン
→ バックエンドからHttpOnly, SameSite=Lax(またはStrict), Secure属性を付け、Cookieに設定
HttpOnly属性によりJavaScriptから読めなくなるため、XSSから守られる(バックエンドからのみ設定可能)。
SameSite属性により外部サイトからのCookie送信を制御し、CSRFを防ぐ。
Secure属性により非暗号化時の送信を禁止し、盗聴を防ぐ。
さらに強固にする必要がある場合は、フロントには一切トークンを持たせず中継サーバを設置する、BFF(Backend For Frontend)という方法もある。
その他の保管場所について
- LocalStorage, SessionStorage
JavaScriptから読めてしまうのでXSSで盗まれる危険性がある - Cookie(HttpOnlyなし)
同様にXSSの恐れがある
実際の処理の流れ
上記の構成でのログイン処理・ログイン後のリクエスト・リフレッシュ処理は以下のようになる。
