😶‍🌫️

OAuth・JWT・CSRF理解ガイド - 初心者エンジニア向け

に公開

はじめに

この記事では、Web開発で必ず出会うOAuthJWTCSRF対策について、初心者でも理解できるようになるべく丁寧に解説します。

「Google認証って何?」「JWTって暗号化されてるの?」「CSRFトークンって必要?」といった疑問を、実際の動きを追いながら解決していきます。

目次

  1. OAuthとは?Google認証との関係
  2. 認可サーバーとリソースサーバー
  3. アクセストークンの仕組み
  4. JWTとセキュリティ
  5. CSRF対策の必要性
  6. LaravelのCSRF対策の仕組み

OAuthとは?Google認証との関係

OAuthの基本概念

OAuth 2.0は、「パスワードを教えずに、他のアプリに自分のデータへのアクセスを許可する仕組み」です。

登場人物

  1. リソースオーナー = あなた(Googleアカウントを持つユーザー)
  2. クライアント = 使いたいアプリ(例:スケジュール管理アプリ)
  3. 認可サーバー = Googleの認証システム
  4. リソースサーバー = GoogleのAPI(Gmail、カレンダー、スプレッドシートなど)

Google認証の実際の流れ

【あるアプリがGoogleカレンダーにアクセスしたい場合】

1. アプリ:「Googleでログイン」ボタンを表示
   ↓
2. ユーザー:ボタンをクリック
   ↓
3. ブラウザ:Googleのログイン画面へ遷移(認可サーバー)
   ↓
4. Google:「このアプリがカレンダーにアクセスすることを許可しますか?」
   ↓
5. ユーザー:「許可する」をクリック
   ↓
6. Google:アプリにアクセストークンを発行
   ↓
7. アプリ:トークンを使ってGoogleカレンダーAPIを呼び出し

メリット

✅ アプリにGoogleのパスワードを教えなくていい
✅ 「カレンダーだけ」「読み取りだけ」など権限を限定できる
✅ 後からいつでも許可を取り消せる


アクセストークンの仕組み

よくある疑問

「アクセストークンを認可サーバーからもらってリソースAPIにアクセスするのは分かるけど、リソースサーバーはどうやってこのトークンが誰のものか判断するの?」

答え1:認可サーバーに問い合わせる

クライアント → リソースサーバー: 
  「トークンABCでカレンダー取得して」
    ↓
リソースサーバー → 認可サーバー: 
  「トークンABCって誰の?有効?」(トークンイントロスペクション)
    ↓
認可サーバー → リソースサーバー: 
  「ユーザー12345、有効、権限OK」
    ↓
リソースサーバー: 
  ユーザー12345のカレンダーを返す

答え2:JWT(自己完結型トークン)

実はGoogleなど多くのサービスはJWT(JSON Web Token)を使っていて、これだと問い合わせ不要です。

JWTの構造

// JWTの中身(Base64デコードすると見える)
{
  "user_id": "12345",
  "email": "user@example.com",
  "scope": "https://www.googleapis.com/auth/calendar.readonly",
  "exp": 1699999999  // 有効期限
}
// + 認可サーバーの秘密鍵による署名

処理の流れ

1. リソースサーバーがJWTを受け取る
2. 認可サーバーの公開鍵で署名を検証(改ざんされていないか)
3. 有効期限をチェック
4. トークン内のuser_idを見てデータを返す

※ 認可サーバーへの問い合わせ不要!高速!

JWTとセキュリティ

よくある疑問

「公開鍵で誰でも検証できるなら、セキュリティ的に危なくない?」

答え:危なくありません

重要な区別

  • 誰でも検証できる誰でも偽造できる
❌ 攻撃者がやりたいこと
  「user_id: 99999(他人のID)」という偽トークンを作る
  → 秘密鍵がないので署名を作れない
  → リソースサーバーで検証失敗

✅ 正規のトークン
  認可サーバーが秘密鍵で署名
  → 誰でも公開鍵で「これは本物だ」と確認できる
  → でも中身は変更できない

トークンの中身を見られても大丈夫

JWTはBase64エンコードされているだけなので、デコードすれば誰でも中身を見られます。

// デコードすると...
{
  "user_id": "12345",
  "email": "user@example.com"
}

でも問題ない理由:

  • ✅ 見られても問題ない情報しか入っていない
  • ❌ パスワードは入っていない
  • ❌ 個人的なデータは入っていない

本当のセキュリティ対策

JWTの署名ではなく、以下で守られています。

1. HTTPS通信

🔒 盗聴防止:通信内容が暗号化
🔒 なりすまし防止:接続先が本物のサーバーか確認
🔒 改ざん防止:途中でデータを書き換えられない

2. トークンの適切な管理

  • 短い有効期限(1時間など)
  • httpOnlyクッキーに保存
  • リフレッシュトークンで更新

3. 二重の防御

HTTPS(TLS/SSL)
└─ 通信中の盗聴・改ざん防止

JWT の署名
└─ トークンの偽造・改ざん防止

CSRF対策の必要性

CSRF攻撃の例

<!-- 悪意のあるサイト evil.com -->
<form action="https://your-bank.com/transfer" method="POST">
  <input name="to" value="攻撃者の口座">
  <input name="amount" value="100万円">
</form>
<script>
  document.forms[0].submit(); // 自動送信
</script>

何が起きるか

1. ユーザーが銀行サイトにログイン中
2. 悪意のあるサイト evil.com を開く
3. ブラウザが自動的にCookie(JWTやセッションID)を送信
4. サーバーは「認証済みユーザーだ」と判断
5. 送金が実行されてしまう ⚠️

問題点: JWTがCookieに入っていると、ブラウザが勝手に送ってしまう

使い分け

ケース1:SPA(React等)でJWT

// ✅ localStorage に保存 → CSRF対策不要
localStorage.setItem('token', jwt);

// リクエスト時に明示的にヘッダーに付ける
fetch('/api/transfer', {
  headers: {
    'Authorization': `Bearer ${jwt}`  // ブラウザが勝手に送らない
  }
});

ケース2:従来のフォーム送信(Laravel等)

<!-- セッションベース認証 + CSRFトークンが必須 -->
<form method="POST" action="/transfer">
    @csrf
    <input name="amount" value="1000">
    <button>送金</button>
</form>

LaravelのCSRF対策の仕組み

@csrf の正体

<form method="POST" action="/transfer">
    @csrf  <!-- これが重要 -->
    <input name="amount" value="1000">
    <button>送金</button>
</form>

実際に生成されるHTML:

<form method="POST" action="/transfer">
    <input type="hidden" name="_token" value="ランダムな文字列ABC123...">
    <input name="amount" value="1000">
    <button>送金</button>
</form>

動作の流れ

1. ページ表示時

サーバー側:
1. ランダムなCSRFトークンを生成(例: token_A)
2. セッションに保存
3. フォームに埋め込む

2. フォーム送信時

サーバー側:
1. セッションに保存されているトークン(token_A)を取得
2. フォームから送信されたトークン(_token)を取得
3. 両者を比較
4. ✅ 一致 → 処理続行
   ❌ 不一致 → 419エラー(CSRF token mismatch)

3. 攻撃が防げる理由

悪意のあるサイトからのリクエスト:
- セッションのトークンを読めない(同一オリジンポリシー)
- だから正しいトークンを送れない
- リクエストが拒否される ✅

セッションの管理

複数ユーザーの同時アクセス

重要: セッションはユーザーごとに別々に管理されます。

Aさんがログイン画面を開く:
1. サーバー:セッションID「abc123」を生成
2. サーバー:CSRFトークン「token_A」を生成
3. DB:セッションID abc123 に token_A を保存
4. レスポンス:
   - Cookie: session_id=abc123
   - HTML: <input value="token_A">

Bさんがログイン画面を開く(同時刻):
1. サーバー:セッションID「xyz789」を生成(別のID!)
2. サーバー:CSRFトークン「token_B」を生成
3. DB:セッションID xyz789 に token_B を保存
4. レスポンス:
   - Cookie: session_id=xyz789
   - HTML: <input value="token_B">

DBの状態

sessions テーブル
┌─────────────┬──────────────────────┬─────────────┐
│ session_id  │ payload (暗号化)     │ last_activity│
├─────────────┼──────────────────────┼─────────────┤
│ abc123      │ {csrf: "token_A"}    │ 1699999999  │
│ xyz789      │ {csrf: "token_B"}    │ 1699999999  │
└─────────────┴──────────────────────┴─────────────┘

検証の流れ

Aさんがログインボタンを押す

リクエスト:
- Cookie: session_id=abc123
- Body: _token=token_A, email=a@example.com, password=xxx

サーバー側:
1. Cookieから session_id=abc123 を取得
2. DBからセッション abc123 のデータを取得
3. セッション内のcsrf_token(token_A)と
   フォームの_token(token_A)を比較
4. ✅ 一致 → ログイン処理続行

攻撃者が悪意のあるサイトから送信

リクエスト:
- Cookie: session_id=abc123(ブラウザが自動送信)
- Body: _token=攻撃者が適当に作った値

サーバー側:
1. Cookieから session_id=abc123 を取得
2. DBからセッション abc123 のデータを取得(token_A)
3. セッション内のtoken_Aと、フォームの偽トークンを比較
4. ❌ 不一致 → 419エラー(CSRF token mismatch)

セッションの保存場所

Laravelでは.envSESSION_DRIVERで設定できます:

# ファイルに保存
SESSION_DRIVER=file

# データベースに保存
SESSION_DRIVER=database

# Redisに保存
SESSION_DRIVER=redis

# Cookieに暗号化して保存
SESSION_DRIVER=cookie

まとめ

OAuth・JWT・CSRFの関係

【OAuth】
└─ 認証・認可の仕組み全体
   ├─ 認可サーバー:トークン発行
   └─ リソースサーバー:APIアクセス

【JWT】
└─ アクセストークンの一種
   ├─ 自己完結型(署名で検証)
   └─ HTTPS + 署名で二重に保護

【CSRFトークン】
└─ フォーム送信の正当性を証明
   ├─ OAuthやJWTとは別物
   └─ Cookieベース認証で必須

実務での注意点

OAuth/JWT実装時

  • ✅ HTTPSは必須(開発環境でも)
  • ✅ トークンの保存場所に注意
  • ✅ 有効期限を短く、リフレッシュトークンで更新

CSRF対策

  • ✅ Cookieベース認証では必須
  • ✅ Laravelなら@csrfを忘れずに
  • ✅ Authorization ヘッダーで送るJWTはCSRF対策不要

おわりに

OAuth、JWT、CSRF対策は、Web開発において必須の知識です。

エンジニアとしてのキャリアの第一歩として、セキュリティを意識した開発を心がけたいです!

引き続き、学習頑張ります🔥

Discussion