ログインできることと、操作してよいことは別
認証は「この人は誰か」を確認する仕組みです。一方で認可は「その人が、この対象に、この操作をしてよいか」を判断する仕組みです。
ログイン機能が正しく動いていても、認可が弱いと次のような事故が起きます。
- 一般ユーザーが管理者APIを呼び出せる
- 他人の注文・請求書・プロフィールをURL変更だけで閲覧できる
- 退職者や契約終了ユーザーが古い権限を持ち続ける
- 画面では非表示のボタンが、API直叩きでは実行できる
認可はUIの表示制御ではなく、サーバー側で守るべきセキュリティ境界です。
認可判断の3要素
認可は、次の3つを組み合わせて判断します。
| 要素 | 意味 | 例 |
|---|---|---|
| Subject | 誰が | user_123, admin, support_agent |
| Action | 何をするか | read, create, update, delete, approve |
| Resource | 何に対して | invoice_456, project_789, user_profile |
たとえば「山田さんは請求書456を編集してよいか」は、次のように表せます。
Subject: user_123
Action: update
Resource: invoice_456
Decision: allow / deny
重要なのは、「ユーザーがログイン済みか」だけで判断しないことです。ログイン済みでも、対象リソースに対する権限がなければ拒否します。
権限モデルの基本パターン
RBAC: ロールで管理する
RBAC(Role-Based Access Control)は、ユーザーにロールを付与し、ロールごとに操作権限を定義する方式です。
| ロール | できること |
|---|---|
admin | 全ユーザーの閲覧・編集・削除 |
manager | 所属チームの閲覧・承認 |
member | 自分のデータ閲覧・編集 |
viewer | 閲覧のみ |
RBACは分かりやすく、業務システムでもよく使われます。ただし、ロールが増えすぎると管理が難しくなります。
users
- id
- name
roles
- id
- name
permissions
- id
- action
- resource_type
user_roles
- user_id
- role_id
role_permissions
- role_id
- permission_id
ABAC: 属性で判断する
ABAC(Attribute-Based Access Control)は、ユーザーやリソースの属性を見て判断します。
「請求書の部署ID」と「ユーザーの所属部署ID」が一致する場合だけ閲覧できる
「金額が100万円以上」の承認は部長以上だけ可能
「公開状態がdraft」の記事は作成者と編集者だけ閲覧できる
RBACだけでは表現しにくい細かな条件を扱えますが、ルールが複雑になりやすいため、ポリシーを分かりやすく管理する設計が必要です。
所有者チェック
最も基本的で、最も漏れやすいのが所有者チェックです。
// 危険: IDだけで取得している
const invoice = await db.invoice.findUnique({
where: { id: invoiceId },
});
この実装では、ログインユーザーが別ユーザーの invoiceId を知っていれば閲覧できる可能性があります。これは IDOR(Insecure Direct Object Reference)と呼ばれる典型的な認可不備です。
// 安全: ログインユーザーとの関係も条件に含める
const invoice = await db.invoice.findFirst({
where: {
id: invoiceId,
ownerId: currentUser.id,
},
});
if (!invoice) {
throw new ForbiddenError("この請求書を閲覧する権限がありません");
}
認可チェックをどこに置くか
認可チェックは、UIではなくAPI・ユースケース層・データアクセス層の境界に置きます。
避けたい実装
// UIでは削除ボタンを非表示にしている
{user.role === "admin" && <button>削除</button>}
// しかしAPIでは権限を見ていない
app.delete("/users/:id", async (req, res) => {
await deleteUser(req.params.id);
res.sendStatus(204);
});
画面上のボタンを隠しても、APIを直接呼ばれれば実行できてしまいます。
守りやすい実装
app.delete("/users/:id", async (req, res) => {
const currentUser = requireLogin(req);
await authorize(currentUser, "delete", {
type: "user",
id: req.params.id,
});
await deleteUser(req.params.id);
res.sendStatus(204);
});
権限判断を authorize() に集約すると、APIごとに条件が散らばりにくくなります。
安全な認可設計の原則
デフォルト拒否
迷ったら許可ではなく拒否します。
function can(user: User, action: Action, resource: Resource): boolean {
const rule = findRule(user, action, resource);
if (!rule) return false;
return rule.allows;
}
「ルールが見つからない場合は許可」は危険です。新しいAPIや新しいリソースを追加したとき、認可ルールの定義漏れがそのまま脆弱性になります。
最小権限
ユーザーやシステムには、必要最小限の権限だけを与えます。
- 読み取り専用ユーザーに更新権限を付けない
- 管理者権限を日常業務のアカウントに付けっぱなしにしない
- バッチ処理用トークンに全API権限を持たせない
- 一時的な権限には期限を設定する
権限は「便利だから広めに付ける」のではなく、「必要になったら追加する」考え方が安全です。
監査ログを残す
重要操作は、誰が、いつ、何に対して、何をしたかを残します。
{
"actorId": "user_123",
"action": "approve",
"resourceType": "invoice",
"resourceId": "invoice_456",
"result": "allow",
"timestamp": "2026-04-26T09:00:00Z"
}
監査ログは、障害調査・不正操作の追跡・権限棚卸しに役立ちます。特に管理画面、承認、削除、個人情報閲覧では必須に近い要件です。
権限設計チェックリスト
設計・レビュー時は次の観点を確認します。
□ API側で必ず認可チェックしているか
□ URLのIDを変えるだけで他人のデータにアクセスできないか
□ ロールだけでなく、所有者・部署・状態なども見ているか
□ デフォルト拒否になっているか
□ 管理者権限が広すぎないか
□ 退職・異動・契約終了時に権限を外せるか
□ 重要操作の監査ログを残しているか
□ テストで「許可されるケース」と「拒否されるケース」を両方確認しているか
まとめ
認可は、ログイン後の安全性を支える最後の砦です。
- 認証は「誰か」、認可は「何をしてよいか」を判断する
- 権限判断は
subject / action / resourceで整理する - UIの非表示だけで守らず、API側で必ず検証する
- デフォルト拒否・最小権限・監査ログを設計に組み込む
認可不備は、画面上では正常に見えても、APIやID指定で簡単に露呈します。仕様書や画面設計の段階から「誰が、どのデータに、どの操作をしてよいか」を明文化しましょう。
認証方式の整理は認証・認可の仕組み、主要なWeb脆弱性はWebセキュリティ基礎で学べます。