Web アプリを開発していると、「JWT」や「Cookie」という言葉を目にすることはありませんか?
ログイン機能を実装するときや、認証周りのコードを読んでいると、よく登場する仕組みです。
ただ、最初はこんな疑問が出てくるのではないでしょうか。
- JWT って、そもそも何のために使うの?
- Cookie と JWT は何が違うの?
- ログインした情報は、どこに保存されているの?
- 署名に使う秘密の値は、どこからやってくるの?
今回は、こうした疑問を解消するために、JWT と Cookie の仕組みを順番に整理していきます!
HTTP のリクエストやレスポンス、JSON の基本を知っている方であれば理解できるように、具体例を交えながら説明していきます。
1. JWT とは?
まずは、JWT から見ていきましょう。
JWT(JSON Web Token)は、利用者の情報などを含み、サーバーが署名を検証することで、内容が改ざんされていないかを確認できるトークンです。
トークンとは、本人確認やアクセス許可などに利用する、いわば「証明書」のようなものです。
イベント会場のリストバンドで考えてみる
JWT の仕組みは、イベント会場のリストバンドに例えると分かりやすいです。
例えば、イベント会場に入るとき、毎回受付で名前や身分証明書を確認してもらうのは大変ですよね。
そこで、最初に受付で本人確認を済ませ、リストバンドを付けてもらいます。
一度リストバンドを付けてもらえば、会場内のエリアに入るたびに、毎回受付で本人確認をする必要はありません。
Web アプリのログインも、これに似ています。
- ログイン:受付で本人確認をする。
- JWT の発行:本人確認が済んだことを示すリストバンドを渡す。
- API へのリクエスト:リストバンドを提示する。
- JWT の検証:リストバンドが本物か、有効期限内かを確認する。
ここでいう API とは、画面からのリクエストを受け取り、データの取得や更新などを行う仕組みです。
なぜ JWT が必要なの?
そもそも、なぜ JWT のような仕組みが必要なのでしょうか?
理由の1つは、HTTP が基本的にリクエストごとに独立しているからです。
例えば、サンプルアプリにログインして、利用者情報を取得したあと、設定を変更するとします。
1. ログインする
↓
2. 利用者情報を取得する
↓
3. 設定を変更する
HTTP では、前のリクエストでログインしたからといって、次のリクエストでも自動的にログイン済みだと分かるわけではありません。
かといって、リクエストを送るたびにパスワードを送信するのは避けたいですよね。
そこで、最初のログイン時に JWT を発行し、次回以降のリクエストでは、その JWT を提示してもらいます。
サーバーは JWT を検証することで、パスワードを毎回送信させることなく、認証情報が有効かどうかを確認できるようになります。
これが、JWT を使う基本的な考え方です!
2. JWT の中身を見てみよう
では、JWT の中には何が入っているのでしょうか?
JWT は、基本的に次の3つの部分から構成されています。
ヘッダー.ペイロード.署名
それぞれの役割を整理してみましょう。
| 部分 | 役割 | 含まれる情報の例 |
|---|---|---|
| ヘッダー | トークンの種類や署名方式を示す | JWT であること、署名アルゴリズム |
| ペイロード | 利用者に関する情報などを保持する | 利用者 ID、権限、有効期限 |
| 署名 | 内容が改ざんされていないかを検証する | 秘密の値を使って計算した署名 |
それぞれの情報が組み合わさることで、JWT という1つの文字列になります。
ペイロードには何を入れるの?
ペイロードには、利用者を識別する ID や権限、有効期限などを格納できます。
今回は、サンプルアプリに「管理者」と「一般利用者」がいるとしましょう。
管理者の場合は、例えば次のような情報を持たせます。
{
"id": 1,
"loginId": "admin001",
"role": "admin",
"exp": 1793000000
}
一方、一般利用者の場合は、次のような形です。
{
"id": 100,
"userNo": "USER001",
"role": "user",
"exp": 1793000000
}
それぞれの項目の意味は、次のとおりです。
| 項目 | 意味 |
|---|---|
id |
利用者を識別する ID |
loginId |
管理者のログイン ID |
userNo |
一般利用者を識別する番号 |
role |
利用者の権限や種類 |
exp |
JWT の有効期限を示す UNIX 時刻(秒) |
もちろん、実際にどの項目を入れるかは、アプリの設計によって変わります。
ここでは、利用者の種類によって保持する情報が異なる例として考えてください。
ここで注意! ペイロードは秘密ではない
JWT を理解するうえで、ぜひ覚えておきたいことがあります。
一般的な署名付き JWT のペイロードは、暗号化されていません。
JWT は Base64URL という形式を使ってデータを表現していますが、これは暗号化とは違います。
つまり、JWT を入手した人がペイロードをデコードすれば、中身を読めてしまう可能性があります。
そのため、次のような情報を入れるのは避けましょう。
- パスワード
- パスワードの再設定に使う秘密情報
- クレジットカード情報
- その他、第三者に知られてはいけない情報
署名が付いているからといって、中身まで秘密になるわけではありません。
JWT には、認証や認可に必要な最小限の情報だけを入れるようにしましょう!
3. 署名ってどういう仕組み?
JWT の3つの要素のうち、特に重要なのが「署名」です。
署名は、JWT の内容が改ざんされていないかを確認するために使います。
ここでは、発行側と検証側が同じ秘密の値を使う方式を例に説明します。
JWT を発行するとき
サーバーは、ヘッダーとペイロードを用意し、秘密の値を使って署名を計算します。
イメージとしては、次のようなものです。
署名 = 計算(ヘッダー + ペイロード, 秘密の値)
実際の JWT では、ヘッダーとペイロードを Base64URL 形式に変換し、ドットで連結した文字列を署名アルゴリズムへの入力として使用します。
上の式は、仕組みを理解するための簡略化した表現です。
JWT を受け取ったとき
では、サーバーは受け取った JWT が本物かどうか、どのように確認するのでしょうか?
基本的な流れは次のとおりです。
- JWT をヘッダー、ペイロード、署名に分ける。
- ヘッダーの署名アルゴリズムなどを確認する。
- ヘッダーとペイロードから署名を計算し直す。
- 計算した署名と、届いた署名を比較する。
- 有効期限なども確認する。
JWT を受け取る
↓
署名を計算し直す
↓
届いた署名と比較する
|
├─ 一致しない → 拒否する
|
└─ 一致する
↓
有効期限を確認する
|
├─ 期限切れ → 拒否する
|
└─ 有効 → 次の確認へ
署名が一致しなければ、JWT が改ざんされている可能性があります。その場合、認証情報として受け入れてはいけません。
認証できない場合は、通常 401 Unauthorized などの HTTP ステータスコードを返します。
ただし、署名が一致しただけで、すべての処理を許可してよいわけではありません。トークンの有効期限や用途、利用者の権限なども確認する必要があります。
ペイロードを書き換えたらどうなる?
例えば、一般利用者の JWT に、次の情報が入っていたとします。
{
"id": 100,
"role": "user"
}
ここで、role を admin に書き換えたらどうなるでしょうか?
「これで管理者になれるのでは?」と思うかもしれません。
しかし、正しく署名を検証していれば、管理者にはなれません。
ペイロードを書き換えると、署名の計算に使うデータも変わります。そのため、元の署名と一致しなくなり、改ざんを検出できるからです。
ただし、これはサーバーが正しく署名を検証していることが前提です。署名を確認せずにペイロードだけを信用する実装では、この仕組みは機能しません。
有効期限も忘れずに確認する
JWT には、exp という有効期限を示す情報を設定できます。
署名が正しくても、有効期限を過ぎた JWT は拒否しなければなりません。
署名が正しい + 有効期限内
→ 次の認証・認可処理へ
署名が正しくない
→ 拒否
有効期限が切れている
→ 拒否
つまり、署名は「改ざんされていないか」を確認するためのもの、有効期限は「まだ利用できるか」を確認するためのものです。
この2つは、セットで理解しておきましょう!
4. 署名に使う秘密の値はどこにあるの?
ここまで読んで、こんな疑問が出てきた方もいるのではないでしょうか。
「JWT を発行するたびに、署名に使う秘密の値も新しく作られるの?」
答えは、通常の共有秘密鍵方式では いいえ です。
秘密の値は事前に設定しておく
秘密の値は、JWT を発行する前に用意しておきます。
サンプルアプリでは、環境変数 AUTH_SECRET に設定しておくことにしましょう。
環境変数とは、アプリケーションの外部から設定値を渡すための仕組みです。
例えば、次のような流れになります。
AUTH_SECRET に秘密の値を設定する
↓
サーバーを起動する
↓
サーバーが秘密の値を読み込む
↓
JWT の発行・検証に利用する
サーバーは起動時などに秘密の値を読み込み、メモリ上で利用します。
JWT を発行するときも、受け取った JWT を検証するときも、基本的には同じ秘密の値を使います。
なお、ここでいう「固定」とは、通常の運用中に勝手に変わらないという意味です。漏えいなどの理由があれば、秘密の値を変更することもあります。
JWT を何枚発行しても、サーバーが保存する必要はない
もう1つ、押さえておきたいポイントがあります。
この方式では、サーバーは発行した JWT を1つずつ保存しておく必要がありません。
例えば、100 人の利用者がログインしたとします。
サーバー
└─ AUTH_SECRET:共有する秘密の値
利用者 A
└─ JWT-A
利用者 B
└─ JWT-B
利用者 C
└─ JWT-C
...
利用者ごとに異なる JWT が発行されますが、サーバーは秘密の値を使って署名を検証できます。
JWT 自体は利用者側が保持し、API へのリクエストと一緒に送信します。
このように、サーバーがログイン状態を個別に保持しなくても検証できる方式を、ステートレスな認証と呼びます。
ただし、ログアウトした時点でトークンを無効にしたい場合などは、別途サーバー側で失効情報を管理する設計もあります。
秘密の値について知っておきたい3つのこと
秘密の値の仕組みが分かったところで、重要なポイントを3つ整理しておきましょう。
① 秘密の値を変えると、それまでの JWT は原則として無効になる
以前の秘密の値で発行された JWT は、新しい秘密の値では正しく検証できません。
そのため、秘密の値を変更すると、利用者は再ログインが必要になる場合があります。
② 秘密の値が漏れると、JWT を偽造される危険がある
秘密の値を入手した人は、その値を使って正しい署名を持つ JWT を作成できる可能性があります。
秘密の値は、ソースコードや公開資料などに書かず、安全に管理する必要があります。
③ サーバーが複数台でも、同じ秘密の値を共有すれば検証できる
例えば、サンプルアプリの API サーバーが2台ある場合を考えます。
ブラウザ
|
v
API サーバー A
AUTH_SECRET = X
API サーバー B
AUTH_SECRET = X
両方のサーバーが同じ秘密の値を使っていれば、サーバー A が発行した JWT をサーバー B でも検証できます。
ただし、秘密の値を安全に共有し、不要な人が参照できないようにする仕組みも必要です。
なお、公開鍵と秘密鍵を使う署名方式では、発行側と検証側で異なる鍵を利用できます。今回は、初学者にも分かりやすい共有秘密鍵方式を例にしています。
5. JWT はどうやってブラウザに渡すの?
JWT を発行できたら、次はブラウザに渡す必要があります。
主な方法は2つあります。
方法① Cookie として返す
1つ目は、HTTP レスポンスの Set-Cookie ヘッダーを使って、JWT を Cookie として返す方法です。
Cookie は、ブラウザが保存し、条件に応じてリクエストと一緒にサーバーへ送信するデータです。
例えば、ログインに成功したときに、次のようなレスポンスを返します。
HTTP/1.1 200 OK
Set-Cookie: token=eyJ...; HttpOnly; Secure; Path=/
この例では、token という名前で JWT を Cookie に保存するよう、ブラウザに指示しています。
それぞれの属性には、次のような意味があります。
| 属性 | 意味 |
|---|---|
HttpOnly |
JavaScript から Cookie の値を直接読み取れないようにする |
Secure |
HTTPS 通信でのみ Cookie を送信する |
Path=/ |
ドメイン内のすべてのパスを対象にする |
ブラウザは、このレスポンスを受け取ると、Cookie の設定に従って保存します。
そして、対象となる API にリクエストを送るときには、条件を満たせば自動的に Cookie を付けてくれます。
GET /user/profile HTTP/1.1
Host: api.example.com
Cookie: token=eyJ...
この方法の便利なところは、画面の JavaScript が JWT の文字列を直接扱わなくてもよいことです。
ただし、Cookie が必ず送信されるわけではありません。ドメインやパス、SameSite などの設定によって送信条件が変わります。
方法② JSON の本文で返す
もう1つは、JWT を JSON のレスポンス本文に含めて返す方法です。
例えば、次のようなレスポンスです。
{
"accessToken": "eyJ..."
}
この場合、画面のプログラムが JWT を受け取り、API へのリクエストに設定します。
GET /user/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ...
このように、Authorization ヘッダーに Bearer と JWT を設定して送信する方法があります。
ただし、この方式では、画面のプログラムが JWT を扱うことになります。
例えば、JavaScript から読み取れる場所に JWT を保存すると、XSS(クロスサイトスクリプティング)によって悪意のある JavaScript が実行された場合に、JWT を盗まれる危険があります。
そのため、JWT をどこに保存するかも重要な設計ポイントです。
Cookie と JSON、どちらを使えばよいの?
それぞれの特徴を整理してみましょう。
| 比較項目 | Cookie で返す | JSON 本文で返す |
|---|---|---|
| トークンの管理 | ブラウザが Cookie として管理する | 画面のプログラムが受け取る |
| リクエストへの付与 | 条件を満たすと自動送信 | 通常はプログラムが設定する |
HttpOnly |
設定できる | JSON 自体には設定できない |
| 主な注意点 | CSRF、Cookie の送信条件 | XSS、トークンの保存方法 |
どちらがよいかは、アプリの構成やセキュリティ要件によって変わります。
Cookie を使えばすべて安全になるわけではありませんし、JSON で返す方法が必ず危険というわけでもありません。
それぞれの特徴を理解して、適切な方法を選ぶことが大切です。
HttpOnly を設定すれば安心?
Cookie を使う場合、特に重要なのが HttpOnly です。
HttpOnly を設定すると、画面の JavaScript から Cookie の値を直接読み取れなくなります。
そのため、XSS が発生した場合でも、JWT を直接読み取られるリスクを減らせます。
ただし、これだけで XSS の影響をすべて防げるわけではありません。悪意のある JavaScript が利用者のブラウザから不正なリクエストを送信する可能性は残ります。
また、Cookie はブラウザによって自動送信されるため、CSRF(クロスサイトリクエストフォージェリ)という攻撃にも注意が必要です。
CSRF とは、利用者の意図とは関係なく、ブラウザに認証情報を送信させて不正な操作を行わせる攻撃です。
Cookie を使う場合は、HttpOnly だけでなく、CSRF 対策も合わせて考えましょう!
なお、Cookie と JSON 本文の両方に同じ JWT を入れて返すと、画面の JavaScript が JSON 本文から JWT を取得できてしまいます。その場合、Cookie に HttpOnly を設定していても、JSON 経由での漏えいを防ぐことはできません。
6. Cookie はどこに保存されるの?
ここで、もう1つ気になることがあります。
「Cookie はサーバーが発行するものだけど、実際にはどこに保存されているの?」
結論からいうと、Cookie は ブラウザを動かしている利用者の端末に保存されます。
サーバーの中に Cookie が保存されているわけではありません。
例えば、Windows の PC でサンプルアプリにログインした場合、Cookie はその PC 上のブラウザのプロファイルに保存されます。
Windows ではどこに保存されるの?
Chrome、Edge、Firefox では、Cookie を保存する場所が異なります。
Windows での代表的な保存先は、次のとおりです。
| ブラウザ | Cookie データの代表的な保存先 |
|---|---|
| Chrome | C:\Users\<ユーザー名>\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies |
| Edge | C:\Users\<ユーザー名>\AppData\Local\Microsoft\Edge\User Data\Default\Network\Cookies |
| Firefox | C:\Users\<ユーザー名>\AppData\Roaming\Mozilla\Firefox\Profiles\<プロファイル名>\cookies.sqlite |
これらは、ブラウザ内部のデータを保存するためのファイルです。
Chrome と Edge では、Cookie の値が Windows の保護機能などを使って暗号化されて保存されることがあります。Firefox も Cookie を管理するための保護機能を備えています。
そのため、保存先のファイルを開けば、Cookie の値がすべてそのまま読めるというわけではありません。
また、保存先はブラウザのバージョンやプロファイルなどによって変わる場合があります。
開発中に Cookie を確認したい場合は、保存ファイルを直接操作するより、開発者ツールを使うのがおすすめです。
開発者ツールで確認してみよう
Chrome や Edge なら、次の手順で Cookie を確認できます。
- サンプルアプリをブラウザで開く。
-
F12キーを押す。 -
Application(アプリケーション)タブを開く。 -
Storage内のCookiesを選択する。 - 対象のドメインを選択する。
Firefox では、開発者ツールの「ストレージ」などから確認できます。
Cookie の一覧を見ると、名前や値、有効期限、HttpOnly、Secure、SameSite などの情報を確認できます。
実装した Cookie が正しく保存されているか、リクエストに送信されているかを調べるときにも役立ちます。
Cookie はドメインごとに管理される
Cookie は、設定されたドメインやパスなどの条件に従って送信されます。
ここで、サンプルアプリの画面と API が別のサブドメインにあるケースを考えてみましょう。
- 画面:
app.example.com - API:
api.example.com
このような構成では、Cookie の設定によっては API へのリクエストに Cookie が付かないことがあります。
特に、画面と API が異なるサイトに属していて、クロスサイトのリクエストで Cookie を送る必要がある場合は、一般的に次の設定を使います。
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=None; Path=/
SameSite=None は、クロスサイトのリクエストでも Cookie を送信できるようにする設定です。この属性を使う場合は、Secure も必要になります。
ただし、app.example.com と api.example.com は通常、同じサイトに属するサブドメインです。ホスト名が違うからといって、必ずクロスサイトになるわけではありません。
また、SameSite=None を設定しても、ブラウザのサードパーティ Cookie 制限などによって送信されない場合があります。
画面と API の構成に応じて、Cookie の設定と実際のリクエストを確認しましょう。
Cookie と JWT の有効期限は別々
Cookie を扱うときに、もう1つ注意したいことがあります。
それは、Cookie の有効期限と JWT の有効期限は別物だということです。
| 項目 | 意味 |
|---|---|
Cookie の Max-Age
|
ブラウザが Cookie |