Web 認証徹底解説: JWT、セッション、Remember Me
サインアップ、ログイン、アクセス/リフレッシュトークン、セッション ID とリメンバー用トークン、遅延リフレッシュ、ログアウト、そして JWT 方式とサーバーセッション方式の選び方まで。実務で役立つエンドツーエンドガイド。
0. 全体像:裏側で何が起きている?
Web 認証は大きく 3 つの仕事を解くために存在します。
- サインアップ:ユーザーを作る。
- ログイン:本人確認(メール + パスワード、OAuth など)。
- ログイン状態を保つ:リクエストや時間をまたいで認証済みを維持する。
おもしろいのは 3 つ目。どうやって「だれか」を覚え、どれくらいの期間覚えておくのか?
実装は 2 系統ですがパターンは同じです。
- JWT ベース(ステートレス)
- サーバーサイドセッション(ステートフル)
どちらも 2 種類のクレデンシャルを持ちます。
| 短命 | 長命 | 役割 |
|---|---|---|
| アクセストークン(JWT) | リフレッシュトークン | 新しいアクセストークンをもらう |
| セッション ID(セッション方式) | Remember-me トークン | 新しいセッションをもらう |
考え方は同じ、実装だけが違う。
1. フェーズ1 – サインアップ(アカウント作成)
JWT でもセッションでも共通です。
- ユーザーがメール、パスワード、必要なら名前/ロケールを送る。
- サーバーでメール形式、パスワード強度、メールの重複を検証。
- パスワードをハッシュ化(bcrypt/argon2/scrypt)。平文を保存しない。
例:
users
------------------------------------
id | email | password_hash | ...
1 | a@b.com | $2b$10$... | ...
------------------------------------- (任意)メール確認リンクを送る。
サインアップ後、ユーザーは作られますが自動ログインするかどうかは実装次第です。
2. フェーズ2 – ログイン(認証)
どちらの方式でも流れは同じ。
POST /login
{ "email": "a@b.com", "password": "secret123", "remember": true }サーバーがパスワードを検証したら、以下のどちらかを作ります。
- JWT 2 つ(アクセス + リフレッシュ)
- セッション ID(必要なら remember トークンも)
この先はどちらの方式を選ぶかで分岐します。
2.2 Google / OAuth ログインを足す(同じパターン)
OAuth(Google/GitHub など)も同じ流れに差し込めます。
- ユーザーが「Google で続行」をクリック。
- ブラウザが Google へリダイレクトされ同意画面へ。
- Google が認可コードをバックエンドに返す。
- バックエンドがコードをトークン+プロフィールと交換。
- バックエンドがローカルユーザーを作成/紐付けし、自分の認証情報を発行:
- JWT 方式: アクセス + リフレッシュトークンを発行。
- セッション方式: session_id(+長期化したければ remember トークン)。
実装のコツ:
- プロバイダーからのレスポンスは「本人証明」として扱い、ローカルユーザーを必ず保存/リンクする。
- 重複防止のためプロバイダーの ID(例:
google_sub)を保存する。 - Cookie は HttpOnly/SameSite/Secure。プロバイダーのトークンを localStorage に置かない。
- パスワードログインと同じリフレッシュ/remember ルールを適用。OAuth はパスワードチェックを置き換えるだけ。
- パケットレベルの完全版は専用ガイドへ: ソーシャルサインイン(Google 例)。
2.3 従来のメール+パスワード vs Google ログイン(詳細比較)
2.3.1 Google ログインでパスワードは消える?
- あなたのアプリでは、はい。ユーザーはパスワードを作らないし入力もしません。
- ただし Google 上ではパスワードや 2FA などを使います。そこを Google に委譲します。
2.3.2 アプリではパスワードの代わりに何を使う?
- ローカルログインは
email + password_hash。 - Google ログインは検証済みメール + Google の一意な subject (
sub)。 - バックエンドは
google_subを保存/紐付け(例:user.google_sub = "112233445566778899")。 - フローは
google_sub → user_id → あなたのセッション/JWT を発行。ローカルパスワードは不要。
2.3.3 Google ログインでパスワードリセットは?
- 対応不要。ユーザーがパスワードを忘れたら Google で復旧します。
- Google 専用アカウントには「パスワードをお忘れですか」フローは不要です。Google の本人確認を信頼するだけ。
2.3.4 アカウント復旧と本人確認はどうなる?
- あなたのアプリではコード/SMS/パスワードリセットを使わなくなります。
- Google がリカバリー、2FA、デバイスチェック、リスク分析、CAPTCHA、不審ログイン警告、バックアップコードを担当。
- Google のリカバリー/検証スタックをそのまま継承できます。
2.3.5 新規ユーザー確認は?
- Google は ID トークンで検証済みメールを返します。
{
"email": "user@gmail.com",
"email_verified": true
}email_verifiedが true なら、独自のメール/SMS 確認を省略可能。Google の検証を借ります。
2.3.6 Google ログインは安全?
- Google の防御を引き継ぎます: MFA、セキュリティキー(FIDO2/U2F)、デバイス認識、リスクベースアクセス、CAPTCHA/ボット対策、漏洩パスワード検知、不審ログイン通知、リカバリーフロー。
- これを自前で作るのは高コスト。だから多くのチームが Google サインインを好みます。
2.3.7 Google アカウントにアクセスできなくなったら?
- 復旧するまでログインできません。
- 冗長性を用意しましょう: 後からメール+パスワードを紐付けたり、別の OAuth プロバイダー(GitHub/Apple など)を追加してサポートが切り替えられるようにする。
2.3.8 図式サマリー
Google なし
+-----------------------------------------+
| あなたが担当: |
| - パスワードハッシュ |
| - ログイン試行管理 |
| - メール検証 |
| - SMS 検証 |
| - 2FA |
| - パスワードリセット |
| - アカウント復旧 |
| - 不審ログイン検知 |
+-----------------------------------------+
Google ログインあり
+-----------------------------------------+
| Google がすべてのアイデンティティ保護を担当 |
| |
| あなたが担当するのは: |
| - ユーザーレコード |
| - セッション/JWT トークン |
| |
+-----------------------------------------+2.3.9 Google ログインは認証フローのどこに入る?
通常ログイン
email + password -> パスワード検証 -> セッション作成 -> ログイン完了
GOOGLE ログイン
google_sub -> id_token 検証 -> セッション作成 -> ログイン完了最初のステップだけが異なり、その後(セッション/JWT、リフレッシュ/remember、ログアウト)は同じです。
2.3.10 パスワードはなくなる?
- 近年、多くのアプリがパスワードレス/OAuth ファーストを採用: Google、Apple、Microsoft、GitHub、Slack、Notion、Discord、Figma、Linear、Superhuman、さらにマジックリンクやパスキー。
- トレンド: 「Google/Apple/Microsoft/GitHub でログイン」または「マジックリンク/パスキーでログイン」→ パスワードはレガシーへ。
2.3.11 Q&A まとめ
| 質問 | 回答 |
|---|---|
| Google ログインでパスワードは必要? | ローカルパスワード不要。Google の本人証明を信頼する。 |
| リカバリーはどうなる? | Google がパスワードリセットとアカウント復旧を全面担当。 |
| 本人確認は? | Google が検証済みメールを返す(email_verified=true)。 |
| セッション/JWT は必要? | はい。Google ログイン後も自分のトークン/セッションを発行する。 |
| 安全性は? | Google のセキュリティ/リカバリー機構を継承するので高い。 |
3. システム A – JWT ベース認証(ステートレス)
コア概念
- アクセストークン: 署名付き JWT。短命(15〜60 分)。毎リクエストで送る。
- リフレッシュトークン: 長命(7〜30 日)。新しいアクセストークンを得るときだけ使う。
アクセストークン = 今日の入場券。
リフレッシュトークン = 入場券をもらうためのパスポート。
JWT でのログイン
- メール + パスワードを検証。
- アクセストークンを作成(短い exp)。
- リフレッシュトークンを作成(長い exp)。
- HttpOnly Cookie で送信:
access_token=...; HttpOnly; Secure; SameSite=Laxrefresh_token=...; HttpOnly; Secure; SameSite=Lax
各リクエストでアクセストークンを使う
ヘッダーまたは Cookie:
Authorization: Bearer <access_token>
Cookie: access_token=...サーバーは署名と exp を検証し、ユーザーを紐付ける。DB lookup 不要(ステートレス)。
アクセストークン失効時 – 遅延リフレッシュ
- 期限切れアクセストークンでのリクエスト → サーバーが
401 token_expired。 - フロントエンドのインターセプターが
POST /auth/refreshを呼ぶ。 - サーバーがリフレッシュトークンを検証し、新しいアクセストークン(必要なら新しいリフレッシュ)を発行。
- フロントエンドが元リクエストを再送。ユーザーは気づかない。
リフレッシュトークンのローテーション
リフレッシュごとに:
- 古いリフレッシュトークンを検証。
- 新しいアクセス + リフレッシュトークンを発行。
- 古いリフレッシュを使用済み/無効として DB に記録。
JWT でのログアウト
- クライアント: Cookie/localStorage を削除。
- サーバー: リフレッシュトークンを DB で失効させる、またはユーザーの
token_versionを更新して古い JWT を無効化。
JWT の長所と短所
長所: アクセストークンは DB 参照なし。SPA、モバイル、マイクロサービス、サーバーレスに向く。
短所: 失効管理が難しめ。トークンの保存ミスが起きやすい。リフレッシュトークンのセキュリティが重要。
4. システム B – サーバーサイドセッション(ステートフル)
コア概念
session_id: ランダム文字列の Cookie。DB/Redis のユーザー行に紐付く。短命。remember_token: 任意の長命ランダム文字列。DB にはハッシュで保存。新しいセッションを発行するために使う。リフレッシュトークンの同等物。
セッション ID = 今日の入場券。
Remember トークン = 複数日のパス。
セッションでのログイン(Remember Me なし)
- メール + パスワードを検証。
session_idを生成。sessionsテーブルに有効期限つきで保存。- Cookie 設定:
session_id=...; HttpOnly; Secure; SameSite=Lax。 - 期限が切れたら再ログイン。
セッションでのログイン(Remember Me あり)
- メール + パスワードを検証。
session_id(短期)とremember_token(長期)を生成。- 保存:
sessions
session_id | user_id | expires_at
---------------------------------
abcd1234 | 42 | +1 day
remember_tokens
hashed_token | user_id | expires_at
-----------------------------------
HASH(xyz...) | 42 | +30 days- Cookie を設定:
session_id=abcd1234; HttpOnly; Secureremember_token=xyz...; HttpOnly; Secure; Max-Age=30 days
各リクエストでセッションを使う
ブラウザが自動で session_id を送る。サーバーは DB/Redis を参照:
- 見つかり有効なら → 認証済み。必要なら有効期限を延長。
- ない/期限切れ → 一時的に未認証。
スライド式有効期限
有効なリクエストごとに expires_at を延長(例: 現在 +1 日)。アクティブなユーザーはログインしっぱなし。放置すると失効。
セッション期限切れ時 – Remember Me を使う
session_id がない/失効していて remember_token がある場合:
remember_tokenをハッシュして検索。- 有効なら → 自動ログインし、新しい
session_idを発行。必要なら remember もローテーション。 - 無効/なし → 再ログインを要求。
セッションでのログアウト
- セッション行を削除し、
session_idCookie をクリア。 - Remember を使っている場合: 行を削除し、
remember_tokenCookie もクリア。
セッションの長所と短所
長所: 削除で簡単に失効。シンプルな思考モデル。従来型サイトや高セキュリティに向く。
短所: セッションストアが必要。毎リクエストで DB/Redis 参照。分散/モバイルのヘビーな構成では不利。
5. モダン UX の現実 – 「Remember Me」は見えない
多くのプロダクトはデフォルトで「ログイン状態を維持」します。チェックボックスを出さず、長命トークン(remember/リフレッシュ)を静かに使います。敏感な操作ではパスワードや MFA を再入力させることもあります。
プロダクトの選択肢:
- 明示的に「Remember me」チェックを出す
- 何も出さず常に記憶(ユーザーがログアウトするまで)
6. 並べて対応づける
概念マッピング
| JWT ワールド | セッションワールド | メモ |
|---|---|---|
| アクセストークン | セッション ID | 短命クレデンシャル |
| リフレッシュトークン | Remember-me トークン | 長命クレデンシャル |
| 署名検証 | DB/Redis 参照 | バリデーション手順 |
/auth/refresh | Remember トークンで自動ログイン | リフレッシュ経路 |
token_expired | セッション期限切れ | 失敗モード |
比較表
| 項目 | JWT ベース認証 | サーバーサイドセッション |
|---|---|---|
| 短命なもの | アクセストークン | セッション ID |
| 長命なもの | リフレッシュトークン | Remember-me トークン |
| 毎リクエスト送るもの | アクセストークン | セッション ID |
| 検証方法 | 署名/クレーム検証 | DB/Redis lookup |
| サーバー状態 | アクセストークンはなし | セッションストア必須 |
| リフレッシュ | /auth/refresh | Remember 経由で新しいセッション |
| ログアウト | トークン削除/失効 | セッション/Remember 行を削除 |
| 向いているケース | SPA、モバイル、API、マイクロサービス | 従来型 Web、モノリス、高セキュリティ |
7. どちらを選ぶ?
SPA/モバイル/マイクロサービスがあり、セッションストアを持ちたくない、かつリフレッシュトークンを堅牢に扱えるなら JWT。パターン: アクセストークンは HttpOnly Cookie または Authorization ヘッダー、リフレッシュトークンは HttpOnly Cookie、401 をトリガーに遅延リフレッシュ。
単一バックエンドが主で、主にサーバーレンダリング、失効をシンプルにしたいならサーバーセッション。パターン: session_id Cookie、セッションテーブル(または Redis)、継続ログイン用に remember_tokens を追加。
8. 最終的な頭の地図
SIGN UP (ユーザー + ハッシュ済みパスワード作成)
|
v
LOG IN (メール + パスワード検証)
|
v
認証方式を選ぶ
---------------------
| |
v v
JWT ベース認証 サーバーセッション
----------------------- -----------------------
ACCESS TOKEN を作成 SESSION_ID を作成
REFRESH TOKEN を作成 セッションを DB に保存
両方をクライアントへ送信 SESSION_ID Cookie を送信
毎リクエスト: 毎リクエスト:
ACCESS TOKEN を送信 SESSION_ID を送信
署名を検証 DB lookup
期限切れ時: 期限切れ時:
REFRESH TOKEN を使用 REMEMBER TOKEN が有効なら:
/auth/refresh で再発行 新しい SESSION_ID を発行
無効なら再ログイン
ログアウト: ログアウト:
トークンを削除/失効 セッション/Remember 行を削除どちらも同じ要件を満たします。「一度ログインしたら、サインイン状態を保ちつつ安全を維持したい」だけです。