クッキーベース認証とトークンベース認証の比較まとめ
クッキーベース認証
クッキーベース認証のログイン時の流れ
ユーザーがemailとpasswordを送信し、ブラウザにセッションIDが設定されるまでの流れ
詳細
-
POST /login (email, password)
- ユーザーがフォームに入力したemailとpasswordを含むリクエストがWebサーバーに送信
-
ユーザー情報検索
- emailに基づいてデータベースからユーザー情報を取得
-
ユーザー情報
- 該当ユーザーのレコード(例: id, name, email, password_hashなど)
- password_hashはbcryptなどでハッシュ化されたパスワード
-
パスワード照合
- ユーザー入力のpasswordとDBにあるpassword_hashを比較し、一致するかを確認
-
パスワード一致した場合、セッションID生成、保存
-
セッションIDを生成(例: abcd1234)
-
セッションストアにセッションIDとユーザーIDを関連付けて保存
-
例
セッションID ユーザーID abcd1234 11 xyz9876 12 wqwe1234 13
-
-
セッションIDをCookieに設定
- レスポンスヘッダーに
Set-Cookie: session_id=abcd1234; Path=/; HttpOnly
を追加し、ブラウザに送信
- レスポンスヘッダーに
-
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を使い、認証チェックを経て、ログイン済みユーザー向けのリソースを取得するまでの流れ
詳細
- GET /secure (Cookie: session_id=abcd1234)
- ブラウザがセッションID(例: abcd1234)をCookieに含めてリクエストを送信
- セッション情報検索
- Webサーバーがセッションストアに対してセッションID(例: abcd1234)に対応する情報を問い合わせ
- セッションIDをもとにユーザーIDを検索
- セッションストアがセッションIDに関連付けられたユーザー情報(例: ユーザーID 11)を返す
- ユーザー情報を取得
- WebサーバーがユーザーIDを使ってデータベースから詳細なユーザー情報を取得
- ユーザー情報
- 該当ユーザーのレコード(例: id, name, email, password_hash, roleなど)
- ユーザーのアクセス権を判定
- ユーザーのロールや権限を確認し、/secureリソースへのアクセス権があるかチェック
- /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を送信し、ブラウザにアクセストークンが設定されるまでの流れ
詳細
-
POST /login (email, password)
- ユーザーがフォームに入力したemailとpasswordを含むリクエストをWebサーバーに送信
-
ユーザー情報検索
- emailに基づいてデータベースからユーザー情報を取得
-
ユーザー情報
- 該当ユーザーのレコード(例: id, name, email, password_hashなど)
- password_hashはbcryptなどでハッシュ化されたパスワード
-
パスワード照合
- ユーザー入力のpasswordとDBにあるpassword_hashを比較し、一致するかを確認
-
パスワード一致した場合、アクセストークン(JWT等)を生成
- ペイロードにユーザー情報(例: id, role等)を含めることが多い(JWT)
- 例
{ "sub": 11, "name": "John Doe", "role": "admin", "iat": 1673330000, "exp": 1673333600 }
-
アクセストークンをブラウザへ送信
- 生成したアクセストークンをレスポンスボディなどに含めてブラウザへ送信
-
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>
トークンベース認証のログイン後の流れ
ユーザーがブラウザに保存されたアクセストークンを使い、認証チェックを経て、ログイン済みユーザー向けのリソースを取得するまでの流れ
詳細
- GET /secure
- ブラウザが保存したトークンをHTTPヘッダー(例: Authorization: Bearer <token>)に含めてリクエストを送信
- アクセストークンの検証
- Webサーバーでアクセストークンを検証し、署名が正しいか、有効期限内などをチェック
- 署名検証は秘密鍵や公開鍵を使って行う(HMACやRSAなど)
- ユーザー情報を取得
- 検証したアクセストークンからユーザーIDなどを取得
- ユーザー情報
- ユーザーID、ユーザー権限
- ユーザーのアクセス権を判定
- ユーザーのロールや権限を確認し、/secureリソースへのアクセス権があるかチェック
- /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 モバイルアプリ対応)
- (フロントの実装コスト)
クッキーベース認証とトークンベース認証方式の特徴比較
認証方式の分類
- クッキーベース認証(セッション認証)
- トークンベース認証: アクセストークンはクッキーに保存 × 短い有効期限
- トークンベース認証: アクセストークンはクッキーに保存 × ブラックリスト
- トークンベース認証: アクセストークンはクッキーに保存 × リフレッシュトークン(クッキーに保存)
- トークンベース認証: アクセストークンはローカルストレージに保存 × 短い有効期限
- トークンベース認証: アクセストークンはローカルストレージに保存 × ブラックリスト
- トークンベース認証: アクセストークンはローカルストレージに保存 × リフレッシュトークン(クッキーに保存)
- トークンベース認証: アクセストークンはインメモリに保存 × リフレッシュトークン(クッキーに保存)
認証方式 | セキュリティ | 利便性 | スケーラビリティ | トークン漏洩時の対応 | (クロスオリジン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の両リスクを最小化できる
- ユーザーのログイン状態が長く維持され、利便性が高い
- 漏洩時はリフレッシュトークンを失効できるが、発行済みのアクセストークンは一定期間有効で即時無効化は不可
- (フロントエンドでページリロード時にアクセストークンが消えるため、リフレッシュトークンを使った再取得が必須。状態管理が必要)
参考資料
Discussion