🔐

クッキーベース認証とトークンベース認証の比較まとめ

2025/02/26に公開

クッキーベース認証

クッキーベース認証のログイン時の流れ

ユーザーがemailとpasswordを送信し、ブラウザにセッションIDが設定されるまでの流れ

詳細
  1. POST /login (email, password)

    • ユーザーがフォームに入力したemailとpasswordを含むリクエストがWebサーバーに送信
  2. ユーザー情報検索

    • emailに基づいてデータベースからユーザー情報を取得
  3. ユーザー情報

    • 該当ユーザーのレコード(例: id, name, email, password_hashなど)
    • password_hashはbcryptなどでハッシュ化されたパスワード
  4. パスワード照合

    • ユーザー入力のpasswordとDBにあるpassword_hashを比較し、一致するかを確認
  5. パスワード一致した場合、セッションID生成、保存

    • セッションIDを生成(例: abcd1234)

    • セッションストアにセッションIDとユーザーIDを関連付けて保存

    • セッションID ユーザーID
      abcd1234 11
      xyz9876 12
      wqwe1234 13
  6. セッションIDをCookieに設定

    • レスポンスヘッダーにSet-Cookie: session_id=abcd1234; Path=/; HttpOnly を追加し、ブラウザに送信
  7. CookieにセッションIDを設定

    • ブラウザがセッションIDをCookieに保存し、以降のリクエストで送信
コード例
  • バックエンド
import express from 'express';
import session from 'express-session';

...

const app = express();
app.use(express.json());

...

app.post('/login', async (req, res) => {
  const { email, password } = req.body;

  // データベースからユーザー情報を取得
  const user = await db.getUserByEmail(email);
  if (!user) {
    res.status(401).json({ message: 'Unauthorized' });
  }

  // パスワード照合
  const isPasswordValid = await bcrypt.compare(password, user.password_hash);
  if (!isPasswordValid) {
    res.status(401).json({ message: 'Unauthorized' });
  }

  // セッションID生成・保存
  req.session.userId = user.id; 
  // ↑ ここで以下の2つが自動的に行われる
  // - セッションストアに (セッションID → ユーザーID) を保存
  // - レスポンスヘッダーに `Set-Cookie: session_id=abcd1234; Path=/; HttpOnly` を追加し、ブラウザに送信

  res.json({ message: 'Login successful' });
});

app.listen(3000, () => console.log('Server running on port 3000'));
  • フロントエンド
<body>
  <form id="loginForm" method="POST">
    <input type="email" id="email" name="email" required>
    <input type="password" id="password" name="password" required>
    <button type="submit">Login</button>
  </form>
</body>

クッキーベース認証のログイン後の流れ

ユーザーがブラウザに保存されたセッションIDを使い、認証チェックを経て、ログイン済みユーザー向けのリソースを取得するまでの流れ

詳細
  1. GET /secure (Cookie: session_id=abcd1234)
    • ブラウザがセッションID(例: abcd1234)をCookieに含めてリクエストを送信
  2. セッション情報検索
    • Webサーバーがセッションストアに対してセッションID(例: abcd1234)に対応する情報を問い合わせ
  3. セッションIDをもとにユーザーIDを検索
    • セッションストアがセッションIDに関連付けられたユーザー情報(例: ユーザーID 11)を返す
  4. ユーザー情報を取得
    • WebサーバーがユーザーIDを使ってデータベースから詳細なユーザー情報を取得
  5. ユーザー情報
    • 該当ユーザーのレコード(例: id, name, email, password_hash, roleなど)
  6. ユーザーのアクセス権を判定
    • ユーザーのロールや権限を確認し、/secureリソースへのアクセス権があるかチェック
  7. /secureのリソースを返す
    • アクセス権が確認できた場合、リソースをブラウザに返す
コード例
  • バックエンド
app.get('/secure', async (req, res) => {
  // セッションIDをもとにユーザーIDを取得
  const userId = req.session.userId;

  // データベースからユーザー情報を取得
  const user = await db.getUserById(userId);
  if (!user) {
   res.status(403).json({ message: 'Forbidden' });
  }

  // ユーザーのアクセス権を確認
  if (!user.hasAccessToSecureResource) {
   res.status(403).json({ message: 'Forbidden' });
  }

  // リソース返す
  res.json({ message: 'Access granted', user });
});
  • フロントエンド
    省略

クッキーベース認証のポイント

  • ログイン後は、各リクエストでセッションストアとデータベースを参照し、ユーザー情報を取得して認証チェックを行う
  • セッションIDが漏洩しても、不正アクセスを検知すればサーバー側で無効化可能
  • HttpOnlyを適用したクッキーにセッションIDを保存することで、XSSの影響を受けにくい
  • CSRF対策として、CSRFトークンをフロント側でリクエストに含める実装が必要

トークンベース認証

トークンベース認証のログイン時の流れ

ユーザーがemailとpasswordを送信し、ブラウザにアクセストークンが設定されるまでの流れ

詳細
  1. POST /login (email, password)

    • ユーザーがフォームに入力したemailとpasswordを含むリクエストをWebサーバーに送信
  2. ユーザー情報検索

    • emailに基づいてデータベースからユーザー情報を取得
  3. ユーザー情報

    • 該当ユーザーのレコード(例: id, name, email, password_hashなど)
    • password_hashはbcryptなどでハッシュ化されたパスワード
  4. パスワード照合

    • ユーザー入力のpasswordとDBにあるpassword_hashを比較し、一致するかを確認
  5. パスワード一致した場合、アクセストークン(JWT等)を生成

    • ペイロードにユーザー情報(例: id, role等)を含めることが多い(JWT)
    {
      "sub": 11,
      "name": "John Doe",
      "role": "admin",
      "iat": 1673330000,
      "exp": 1673333600
    }
    
  6. アクセストークンをブラウザへ送信

    • 生成したアクセストークンをレスポンスボディなどに含めてブラウザへ送信
  7. Cookieやローカルストレージ等にアクセストークンを保存

    • ブラウザ側で受け取ったアクセストークンをブラウザのCookieやローカルストレージ等に保存し、以降のリクエストで送信
コード例
  • バックエンド
import express from 'express';
import jwt from 'jsonwebtoken';

...

const app = express();
app.use(express.json());

...

const secretKey = process.env.JWT_SECRET

app.post('/login', (req, res) => {
  const { email, password } = req.body;

  // データベースからユーザー情報を取得
  const user = await db.getUserByEmail(email);
  if (!user) {
    res.status(401).json({ message: 'Unauthorized' });
  }
  
  // パスワード照合
  const isPasswordValid = await bcrypt.compare(password, user.password_hash);
  if (!isPasswordValid) {
    res.status(401).json({ message: 'Unauthorized' });
  }

  // アクセストークン(JWT等)を生成
  const token = jwt.sign({ userId: user.id, role: user.role }, secretKey, { algorithm: 'HS256', expiresIn: '1h' });

  // アクセストークンをブラウザへ送信
  res.json({ token });
});

app.listen(3000, () => console.log('Server running on port 3000'));
  • フロントエンド
<body>
  <form id="loginForm">
    <div>
      <label for="email">Email:</label>
      <input type="email" id="email" name="email" required>
    </div>
    <div>
      <label for="password">Password:</label>
      <input type="password" id="password" name="password" required>
    </div>
    <button type="submit">Login</button>
  </form>
</body>
<script>
  const loginForm = document.getElementById('loginForm');

  loginForm.addEventListener('submit', async (event) => {
    event.preventDefault();

    const email = document.getElementById('email').value;
    const password = document.getElementById('password').value;

    try {
      const response = await fetch('/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });

      if (response.ok) {
        const data = await response.json();
        const token = data.token;  // レスポンスからアクセストークンを取得
        localStorage.setItem('authToken', token);  // アクセストークンをローカルストレージに保存
        alert('Login successful!');
      } else {
        alert('Login failed');
      }
    } catch (error) {
      console.error('Error during login:', error);
      alert('An error occurred. Please try again.');
    }
  });
</script>

トークンベース認証のログイン後の流れ

ユーザーがブラウザに保存されたアクセストークンを使い、認証チェックを経て、ログイン済みユーザー向けのリソースを取得するまでの流れ

詳細
  1. GET /secure
    • ブラウザが保存したトークンをHTTPヘッダー(例: Authorization: Bearer <token>)に含めてリクエストを送信
  2. アクセストークンの検証
    • Webサーバーでアクセストークンを検証し、署名が正しいか、有効期限内などをチェック
    • 署名検証は秘密鍵や公開鍵を使って行う(HMACやRSAなど)
  3. ユーザー情報を取得
    • 検証したアクセストークンからユーザーIDなどを取得
  4. ユーザー情報
    • ユーザーID、ユーザー権限
  5. ユーザーのアクセス権を判定
    • ユーザーのロールや権限を確認し、/secureリソースへのアクセス権があるかチェック
  6. /secureのリソースを返す
    • アクセス権が確認できた場合、リソースをブラウザに返す
コード例
  • バックエンド
const secretKey = process.env.JWT_SECRET

app.get('/secure', (req, res) => {
  // HTTPヘッダーからアクセストークンを取得
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    res.status(401).json({ message: 'Unauthorized' });
  }
  const token = authHeader.split[' '](1);

  // アクセストークンの検証
  const decoded = jwt.verify(token, secretKey); // secretKeyで署名検証
  const user = users.find(u => u.id === decoded.userId);

  if (!user) {
    res.status(401).json({ message: 'Unauthorized' });
  }

  // ユーザーのアクセス権を確認
  if (user.role !== 'admin') { // 例えば、adminだけアクセス可能
    res.status(403).json({ message: 'Forbidden' });
  }

  // リソース返す
  res.json({ message: 'Access granted', user });
});
  • フロントエンド
<body>
  <button id="fetchResource">Get Secure Data</button>

  <script>
    document.getElementById('fetchResource').addEventListener('click', async () => {
      const token = localStorage.getItem('token');

      const response = await fetch('/secure', {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        }
      });

      const result = await response.json();
      console.log(result)
    });
  </script>
</body>

トークンベース認証のポイント

  • ログイン後は、各リクエストでアクセストークンを使ってユーザー情報を取得し、認証を行う

    • 各リクエストでアクセストークンの署名を検証し、ユーザーIDや権限を復元
    • セッションストアやデータベースと連携する必要なし
  • ブラウザでのアクセストークン保存場所はインメモリ、ローカルストレージ、クッキーがある

    詳細
    • インメモリ

      • XSSの影響を受けにくい
      • ページリロードでデータが消える
      • クロスオリジンAPI対応: 可能(Authorizationヘッダーで送信)
      • セッション状態をクライアント側で管理するため、SPA向け
      • ログインのたびに新しいアクセストークンが必要になるため、リフレッシュトークンを組み合わせることで利便性を向上
    • ローカルストレージ

      • XSSリスクあり
      • 永続的に保存される
      • クロスオリジンAPI対応: 可能(Authorizationヘッダーで送信)
      • 手軽に実装できるが、セキュリティリスクが高い
    • クッキー

      • CSRFリスクあり
      • クロスオリジンAPI対応: 不可(CORSの影響を受ける)
      • サーバー側からクッキーを削除できるため、ログアウト時に強制的に無効化できる
  • アクセストークンが漏洩した場合、サーバー側で即時無効化できない

    • 一度発行したアクセストークンは、有効期限が切れるまで使えてしまう
    • 対策方法としては、短い有効期限を設定、ブラックリスト方式、リフレッシュトークン方式がある
    詳細
    • 短い有効期限を設定

      • 無効化ではないが、リスクを抑える
      • 有効期限が切れるたびにログインが必要になるため、利便性が低下
    • ブラックリスト方式

      • データベースやキャッシュストアに無効化するアクセストークンのリストを保持し、認証時に照合
      • ログアウト時や異常なアクセスを検知した際に、該当トークンをブラックリストに追加
      • 各リクエストごとにデータベースやキャッシュストアを参照するため、パフォーマンスに影響し、システムの拡張性が低下
      • ログイン後のフロー
    • リフレッシュトークン方式

      • トークン(短い有効期限)とリフレッシュトークン(長期間有効)を発行
      • 通常のリクエストにはアクセストークンを使用し、期限切れ時にリフレッシュトークンで新しいアクセストークンを取得
      • リフレッシュトークンはクッキーに保存
      • アクセストークンの有効期限が切れても、リフレッシュトークンを利用することで、再ログインせずに新しいアクセストークンを取得し、リソースにアクセスできる
      • リフレッシュトークンの期限が切れたときだけ、データベースやセッションストアにアクセスする
      • ログイン後のフロー
  • アクセストークンの保存場所や無効化方法が、以下の項目に影響する

    • セキュリティ
    • アクセストークン漏洩時の対応
    • スケーラビリティ
    • 利便性(ログイン頻度)
    • (クロスオリジンAPI or モバイルアプリ対応)
    • (フロントの実装コスト)

クッキーベース認証とトークンベース認証方式の特徴比較

認証方式の分類

  1. クッキーベース認証(セッション認証)
  2. トークンベース認証: アクセストークンはクッキーに保存 × 短い有効期限
  3. トークンベース認証: アクセストークンはクッキーに保存 × ブラックリスト
  4. トークンベース認証: アクセストークンはクッキーに保存 × リフレッシュトークン(クッキーに保存)
  5. トークンベース認証: アクセストークンはローカルストレージに保存 × 短い有効期限
  6. トークンベース認証: アクセストークンはローカルストレージに保存 × ブラックリスト
  7. トークンベース認証: アクセストークンはローカルストレージに保存 × リフレッシュトークン(クッキーに保存)
  8. トークンベース認証: アクセストークンはインメモリに保存 × リフレッシュトークン(クッキーに保存)
認証方式 セキュリティ 利便性 スケーラビリティ トークン漏洩時の対応 (クロスオリジンAPI or モバイルアプリ対応) (フロントの実装コスト)
1 ×
2
3 ×
4
5 ×
6 × ×
7 ×
8 ×
詳細
  • セキュリティ
    • ○: XSS・CSRFの両リスクを最小化できる
    • △: CSRFリスクあり
    • ×: XSSリスクあり
  • 利便性
    • ○: ユーザーのログイン状態が長く維持され、利便性が高い
    • △: セッションの維持は可能だが、ブラウザを閉じるとログアウトされるなどの制限がある
  • スケーラビリティ
    • ○: サーバー側でセッション情報を持たないため、複数サーバーへの負荷分散やスケールアウトが容易
    • ×: セッション情報を特定のサーバーに保存するため、負荷分散が難しく、スケールアウト時にセッション共有の仕組みが必要
  • トークン漏洩時の対応
    • ○: サーバー側でセッションを無効化できるため、漏洩したトークンの即時無効化が可能
    • △: 漏洩時にリフレッシュトークンを失効させられるが、発行済みのアクセストークンは有効期限内であれば使えてしまうため、即時無効化は不可
    • ×: リフレッシュトークン方式よりもアクセストークンの有効期限が長く、漏洩時のリスクが高い
  • (クロスオリジンAPI or モバイルアプリ対応)
    • ○: クライアントとAPIが異なるオリジンでも制約なく利用可能(例: Authorizationヘッダーによる認証)
    • △: クッキーを利用する場合、CORS設定が必要。モバイルアプリでは基本的に使用できない
      ※ モバイルアプリを作る場合、クッキー認証は選択肢から外れる
  • (フロントの実装コスト)
    • ○: クッキーに保存されるため、フロントエンドでのトークン管理が不要。リクエスト時に自動的に送信され、追加の処理が不要
    • △: フロントエンドでアクセストークンを保持し、リフレッシュトークンを適用してアクセストークンを更新する処理が必要
    • ×: フロントエンドでページリロード時にアクセストークンが消えるため、リフレッシュトークンを使った再取得が必須。状態管理が必要。
      ※ フロントの負担を軽くするならクッキー認証の方が楽

おまけ: アーキテクチャ別の認証方式まとめ

モノリシック(例: Rails) or 同一オリジン(例: Rails + React)

  • 同一オリジン & XSS対策のため、ローカルストレージに保存しない方が良さそう → 適した認証方式は 1, 2, 4
  • 1, 2, 4はいずれもCSRFリスクあり
認証方式 セキュリティ 利便性 スケーラビリティ トークン漏洩時の対応 (フロントの実装コスト)
1 ×
2 ×
4
詳細
  • 1: クッキーベース認証

    • ユーザーのログイン状態が長く維持され、利便性が高い
    • セッション情報を特定のサーバーに保存するため、負荷分散が難しく、スケールアウト時にセッション共有の仕組みが必要
    • フロントエンドでトークンを手動管理し、API リクエスト時に毎回Authorizationヘッダーを設定する必要がある、また。リフレッシュトークンを適切に扱い、アクセストークンの更新処理を実装する必要がある
    • (クッキーに保存されるため、フロントエンドでのトークン管理が不要。リクエスト時に自動的に送信され、追加の処理が不要)
  • 2: トークンベース認証: アクセストークンはクッキーに保存 × 短い有効期限

    • セッションの維持は可能だが、ブラウザを閉じるとログアウトされるなどの制限がある
    • サーバー側でセッション情報を持たないため、複数サーバーへの負荷分散やスケールアウトが容易
    • リフレッシュトークン方式よりもアクセストークンの有効期限が長く、漏洩時のリスクが高い
    • (クッキーに保存されるため、フロントエンドでのトークン管理が不要。リクエスト時に自動的に送信され、追加の処理が不要)

    ※ Railsでは、これに似た仕組みとしてデフォルトでActionDispatch::Session::CookieStoreがあり、secret_key_baseを用いてクッキーを復号・検証し、DBアクセスなしでセッション管理が可能

  • 4: トークンベース認証: アクセストークンはクッキーに保存 × リフレッシュトークン(クッキーに保存)

    • ユーザーのログイン状態が長く維持され、利便性が高い
    • サーバー側でセッション情報を持たないため、複数サーバーへの負荷分散やスケールアウトが容易
    • 漏洩時にリフレッシュトークンを失効させられるが、発行済みのアクセストークンは有効期限内であれば使えてしまうため、即時無効化は不可
    • (フロントエンドでアクセストークンを保持し、リフレッシュトークンを適用してアクセストークンを更新する処理が必要)

SPA(例: React / Next.js + S3)+ APIサーバー(例: Rails)

  • クロスオリジン & XSS対策のため、ローカルストレージに保存しない方が良さそう → 適した認証方式は 2, 4, 8
  • 2, 4, 8はいずれもスケーラビリティが高い
認証方式 セキュリティ 利便性 スケーラビリティ トークン漏洩時の対応 (フロントの実装コスト)
2 ×
4
8 ×
詳細
  • 2: トークンベース認証: アクセストークンはクッキーに保存 × 短い有効期限

    • CSRFリスクあり
    • セッションの維持は可能だが、ブラウザを閉じるとログアウトされるなどの制限がある
    • リフレッシュトークン方式よりもアクセストークンの有効期限が長く、漏洩時のリスクが高い
    • (クッキーに保存されるため、フロントエンドでのトークン管理が不要。リクエスト時に自動的に送信され、追加の処理が不要)
  • 4: トークンベース認証: アクセストークンはクッキーに保存 × リフレッシュトークン(クッキーに保存)

    • CSRFリスクあり
    • ユーザーのログイン状態が長く維持され、利便性が高い
    • 漏洩時はリフレッシュトークンを失効できるが、発行済みのアクセストークンは一定期間有効で即時無効化は不可
    • (フロントエンドでアクセストークンを保持し、リフレッシュトークンを適用してアクセストークンを更新する処理が必要)
  • 8: トークンベース認証: アクセストークンはインメモリに保存 × リフレッシュトークン(クッキーに保存)

    • XSS・CSRFの両リスクを最小化できる
    • ユーザーのログイン状態が長く維持され、利便性が高い
    • 漏洩時はリフレッシュトークンを失効できるが、発行済みのアクセストークンは一定期間有効で即時無効化は不可
    • (フロントエンドでページリロード時にアクセストークンが消えるため、リフレッシュトークンを使った再取得が必須。状態管理が必要)

参考資料

https://www.cloudflare.com/ja-jp/learning/access-management/token-based-authentication/
https://logmi.jp/main/technology/324349
https://www.digitalocean.com/community/tutorials/nodejs-jwt-expressjs
https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/

Discussion