はじめに
「ログインできる」と「操作できる」は、全く別の話です。
認証(Authentication)と認可(Authorization)は、別の処理です。
混ぜた瞬間に、設計は壊れます。
認証と認可を混ぜるのは、
「本人確認できたから、金庫も開くでしょ?」
と言っているのと同じです。
世の中、そんなに親切にはできていません。
よくある会話:
「ログインできたのに、操作できないんだけど?」
→ それ、認証は成功していて、認可で落ちています。
この記事で一貫して伝える主張はこれです:
認証と認可は「順番も責任も目的も完全に別物」
- 似てるから混ざる
- 混ざるから設計が壊れる
- 壊れるとセキュリティ事故になる
この記事では、認証と認可の違いから実務での事故例、正しい実装方法まで、厳しく・詳しく解説します。
認証とは何か(定義を曖昧にしない)
厳密な定義
認証(Authentication)とは:
「あなたは誰か」を確認すること
認証が答える質問は1つだけ
❓ Who are you?
具体例
認証の手段には以下があります:
- ID / パスワード: 最も一般的な認証方法
- OAuthログイン: Google、GitHubなどでのログイン
- 生体認証: 指紋、顔認証
- APIキー: サービス間通信での本人確認
- JWTの署名検証: トークンが改ざんされていないか確認(※中身はまだ見ない)
- 多要素認証(MFA): パスワード + SMSコードなど
認証の責任範囲
認証は「本人確認」で終わりです。
- ✅ このユーザーは確かに
user_id: 123である - ❌ このユーザーが何をしていいかは認証では判断しない
認可とは何か(ここで切り分け)
厳密な定義
認可(Authorization)とは:
「その人が何をしていいか」を決めること
認可が答える質問
❓ What can you do?
具体例
認可の判断には以下があります:
-
管理者だけが削除できる:
role === "admin" -
自分のデータだけ見られる:
user_id === resource.owner_id - GETはOK、DELETEは禁止: HTTPメソッドベースの制御
-
/adminはrole=adminのみ: パスベースの制御 - リソース単位の権限: 記事Aは編集可、記事Bは閲覧のみ
認可の責任範囲
ログインしていても、禁止は禁止です。
- ✅ 認証済みユーザーである
- ✅ しかしこの操作は権限がない → 403 Forbidden
一瞬で分かる対比表
| 項目 | 認証(Authentication) | 認可(Authorization) |
|---|---|---|
| 英語 | Authentication | Authorization |
| 目的 | 誰かを確認 | 何ができるか |
| 質問 | Who are you? | What can you do? |
| 実行タイミング | 最初 | 認証の後 |
| 失敗時 | ログイン不可(401) | 権限エラー(403) |
| 例 | パスワード確認 | 管理者チェック |
| 保証するもの | 本人性 | 権限 |
この表を頭に入れるだけで、混同は激減します。
処理の順番(絶対に外せない)
認証と認可には明確な順番があります。
正しい順番
認証 → 認可 → 処理実行
フロー図で理解する
強調ポイント
- 認証に成功しても、認可で落ちるのは正常
- 認可を先にやることはできない(誰か分からないから)
- 認証なしで認可はありえない(誰に権限を与えるかが不明)
よくある「ごっちゃ事故」
実務でよくある失敗パターンを紹介します。
事故① 「ログインできたから全部OKだと思った」
ログインできた ≠ 管理者
間違った実装:
// 認証だけして、認可をしていない
app.delete('/api/users/:id', (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
// 認証済みなら誰でも削除できてしまう!
deleteUser(req.params.id);
res.json({ success: true });
});
正しい実装:
app.delete('/api/users/:id', (req, res) => {
// 認証チェック
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
// 認可チェック
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
deleteUser(req.params.id);
res.json({ success: true });
});
事故② 認可をフロントだけでやる
フロントでボタン消したから大丈夫
→ 全然大丈夫じゃない
間違った実装:
// フロント側
{user.role === 'admin' && <DeleteButton />}
// API側(認可チェックなし)
app.delete('/api/users/:id', (req, res) => {
deleteUser(req.params.id);
res.json({ success: true });
});
問題点:
- API直叩きで誰でも削除できる
- ブラウザの開発者ツールで簡単に突破される
- フロントの制御は「UI/UX」であって「セキュリティ」ではない
正しい方針:
- フロント: UI制御のみ(ボタンを表示/非表示)
- バックエンド: 必ず認可チェックを実装
なお、この実装は
「ドアは閉めたけど、壁は壊れている」状態です。
UIは目隠しであって、鍵ではありません。
事故③ JWT = 認可だと思っている
誤解:
JWTがあれば権限チェック不要
現実:
- JWTは認証情報の運搬手段
- 中身(claims)を見て判断するのが認可
- トークン ≠ 権限
比喩で理解する:
JWTを検証しただけでは「名札を見ただけ」。鍵を渡すかどうかは、まだ決めていません。
JWTは「通行証」ではありません。「身分証」です。
マイナンバーを見せたところで、その部屋へ入る権限がないのなら入ることは出来ません。
本人確認ができた事実と、その場所に入っていいかどうかは、完全に別問題です。
「でも本人なんですけど?」 は、システムには一切通用しません。
実装例:
// JWTの検証 = 認証
const decoded = jwt.verify(token, SECRET_KEY);
// → { user_id: 123, role: 'user' }
// 中身を見て判断 = 認可
if (decoded.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
実務での具体例(API視点)
認証の例
GET /api/profile HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
サーバー側:
// 署名検証 = 認証
const user = verifyToken(req.headers.authorization);
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 認証OK: このユーザーは user_id: 123 である
認可の例
// 認可チェック
if (user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
// または
if (user.id !== resource.owner_id) {
return res.status(403).json({ error: 'Forbidden' });
}
同じ場所で書かれても、役割は別です。
「認証も認可もやってくれる仕組み」は存在しない
フレームワークは「処理」を助けます。「判断」は絶対にしてくれません。
「roleを発行してくれる」 ことと、 「そのroleで何を許可するか」 は別問題です。
以下のサービスは認証基盤です:
- OAuth / OpenID Connect: 認証プロトコル
- Auth0: 認証SaaS
- Firebase Authentication: Googleの認証サービス
- AWS Cognito: AWSの認証サービス
これらは以下を提供します:
- ✅ ユーザー登録・ログイン
- ✅ トークン発行
- ✅ 本人確認
しかし、以下は提供しません:
- ❌ 「このユーザーが管理者かどうか」の判断
- ❌ 「このリソースを編集できるか」の判断
- ❌ ビジネスロジックに基づく権限制御
認可ロジックは自分で書く必要があります。
実装パターン: 認証と認可の分離
パターン1: ミドルウェアで分離
// 認証ミドルウェア
const authenticate = (req, res, next) => {
const user = verifyToken(req.headers.authorization);
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
req.user = user;
next();
};
// 認可ミドルウェア
const requireAdmin = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// 使用例
app.delete('/api/users/:id', authenticate, requireAdmin, (req, res) => {
deleteUser(req.params.id);
res.json({ success: true });
});
パターン2: デコレーターで分離(NestJS等)
@Controller('users')
@UseGuards(AuthGuard) // 認証
export class UsersController {
@Delete(':id')
@Roles('admin') // 認可
async deleteUser(@Param('id') id: string) {
await this.usersService.delete(id);
return { success: true };
}
}
パターン3: リソースベース認可(ABAC)
const canDeleteUser = (actor, target) => {
// 管理者は誰でも削除可能
if (actor.role === 'admin') return true;
// 自分自身は削除可能
if (actor.id === target.id) return true;
// それ以外は不可
return false;
};
app.delete('/api/users/:id', authenticate, async (req, res) => {
const targetUser = await getUser(req.params.id);
if (!canDeleteUser(req.user, targetUser)) {
return res.status(403).json({ error: 'Forbidden' });
}
deleteUser(req.params.id);
res.json({ success: true });
});
HTTPステータスコードの使い分け
認証と認可の失敗は、異なるステータスコードで返します。
| 状況 | ステータスコード | 意味 |
|---|---|---|
| 認証失敗 | 401 Unauthorized | 本人確認できなかった |
| 認可失敗 | 403 Forbidden | 本人は確認できたが権限がない |
| 未ログイン | 401 Unauthorized | トークンがない・無効 |
なぜこの区別が重要か
401と403を正しく分けることで:
- フロント: 「再ログインすべきか」を判断できる
- ログ: 「攻撃か誤操作か」を区別できる
- 運用: 問題の切り分けが正確になる
よくある間違い:
// ❌ 認可失敗なのに401を返す
if (user.role !== 'admin') {
return res.status(401).json({ error: 'Unauthorized' });
}
// ✅ 正しくは403
if (user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' });
}
401と403の使い分け:
- 401: 「誰だか分からない」 → 再ログインで解決する可能性
- 403: 「誰だか分かるが権限がない」 → 再ログインしても解決しない
よくある誤解まとめ
❌ 認証 = ログイン後の全部
違います。 ログインはスタート地点です。
認証は「誰か」を確認するだけ。「何ができるか」は認可で判断します。
❌ 認可はオプション
違います。 認可はセキュリティそのものです。
認証だけでは「誰でも何でもできる」状態になります。
❌ 権限は画面制御でOK
違います。 APIで必須です。
フロントの制御は簡単にバイパスされます。セキュリティはバックエンドで実装します。
❌ JWTがあれば認可不要
違います。 JWTは認証情報の運搬手段です。
JWTの中身(role、permissionsなど)を見て、認可判断を行う必要があります。
❌ ログインできれば全部見られる
違います。 認証と認可は別です。
ログイン後も、各操作・各リソースごとに権限チェックが必要です。
まとめ
認証は「名乗らせる」だけ。認可は「鍵を渡すかどうか」。
名乗っただけで、金庫は開きません。
システムは感情を汲み取りません。
正しい順番で、正しい判断をしたときだけ、正しく動きます。
覚えておくべき原則
- 認証 ≠ 認可(完全に別物)
- 認証 → 認可 → 処理(順番は固定)
- 認証はログイン、認可は権限(役割が違う)
- フロントは見せない、バックエンドは通さない(防御の場所が違う)
- 401は本人不明、403は権限なし(エラーの意味が違う)
実装前チェックリスト
設計・実装時に必ず確認すべき項目:
- この処理は「誰か」を確認しているか?(認証)
- この処理は「何をしていいか」を判断しているか?(認可)
- 401と403は意図通りに分かれているか?
- フロントだけで完結していないか?(APIでも必ず認可)
- JWTの検証と権限チェックを混同していないか?
- 認証なしで認可を試みていないか?(順番の確認)
認証と認可の関係
認証と認可を混ぜると、仕様も責任も曖昧になります。
曖昧なセキュリティは、必ず破られます。
この記事が、あなたのセキュリティ設計の一助となれば幸いです。
次に学ぶべきトピック
さらに深く学びたい方は、以下のトピックを探求してみてください:
- OAuth / OpenID Connectの仕組み: 認証プロトコルの標準
- JWTの「何が認証で何が認可か」: トークンの中身と使い方
- RBAC vs ABAC: ロールベース vs 属性ベース認可
- 403と401の実装パターン: エラーハンドリングのベストプラクティス
- 権限管理の設計パターン: ACL、RBAC、ABACの選び方
おすすめ学習順:
- OAuth / OpenID Connectの仕組み
- JWTの役割分担(認証と認可)
- RBAC vs ABAC
- 401 / 403 の実装設計
- 権限管理パターン(ACL等)