実践的な RBAC:アプリケーションのための安全な認可 (Authorization) の実装
アプリケーションのための安全かつスケーラブルな認可 (Authorization) システムの実装に悩んでいませんか?ロールベースのアクセス制御 (RBAC) はユーザー権限管理の業界標準ですが、正しく実装するのは難しい場合があります。このチュートリアルでは、実際のコンテンツ管理システム (CMS) を例に、堅牢な RBAC システムの構築方法を紹介します。
このガイドに従うことで、次のことが学べます:
- ✨ 細かな制御が可能な権限設計と実装方法
- 🔒 意味のあるロールへの権限整理のベストプラクティス
- 👤 リソース所有権の効果的な扱い方
- 🚀 認可 (Authorization) システムをスケーラブルかつ保守しやすくする方法
- 💡 実際の CMS を使った実装例
このチュートリアルの完全なソースコードは GitHub で公開されています。
RBAC の基本を理解する
ロールベースのアクセス制御 (RBAC) は、単にユーザーに権限を割り当てるだけではありません。セキュリティと保守性のバランスを取るための、構造化された認可 (Authorization) アプローチを作ることが重要です。
RBAC とは何か については Auth Wiki で詳しく学べます。
本実装で従う主な原則は以下の通りです:
細かな権限設計
細かな権限設計により、システム内でユーザーができることを正確に制御できます。「admin」や「user」といった大まかなアクセスレベルではなく、リソースごとに具体的なアクションを定義します。例:
read:articles
- システム内のすべての記事を閲覧create:articles
- 新しい記事の作成update:articles
- 既存記事の編集publish:articles
- 記事の公開ステータス変更
リソース所有権とアクセス制御
リソース所有権は、CMS の認可 (Authorization) 設計における基本概念です。RBAC でロールごとのアクションを定義しつつ、所有権によってアクセス制御に個人の要素を加えます:
- 著者は自分が作成した記事に自動的にアクセスできます
- この自然な所有モデルにより、著者は常に自分のコンテンツを閲覧・編集できます
- システムは記事操作時にロール権限または所有権のいずれかを確認します
- たとえば
update:articles
権限がなくても、著者は自分の記事を編集できます - この設計により、余分なロール権限を増やさずにセキュリティを維持できます
この二重構造(ロール+所有権)により、より直感的かつ安全なシステムとなります。パブリッシャーや管理者はロール権限で全コンテンツを管理でき、著者は自分の作品をコントロールできます。
安全な API 設計
まず、CMS のコア機能を API エンドポイントで設計します:
GET /api/articles # すべての記事一覧
GET /api/articles/:id # 特定の記事取得
POST /api/articles # 新規記事作成
PATCH /api/articles/:id # 記事編集
DELETE /api/articles/:id # 記事削除
PATCH /api/articles/:id/published # 公開ステータス変更
API へのアクセス制御の実装
各エンドポイントごとに、アクセス制御の 2 つの側面を考慮します:
- リソース所有権 - ユーザーはこのリソースの所有者か?
- ロールベースの権限 - ユーザーのロールでこの操作が許可されているか?
各エンドポイントのアクセス制御は次の通りです:
エンドポイント | アクセス制御ロジック |
---|---|
GET /api/articles | - list:articles 権限を持つユーザー、または著者は自分の記事を閲覧可能 |
GET /api/articles/:id | - read:articles 権限を持つユーザー、または記事の著者 |
POST /api/articles | - create:articles 権限を持つユーザー |
PATCH /api/articles/:id | - update:articles 権限を持つユーザー、または記事の著者 |
DELETE /api/articles/:id | - delete:articles 権限を持つユーザー、または記事の著者 |
PATCH /api/articles/:id/published | - publish:articles 権限を持つユーザーのみ |
スケールする権限システムの設計
API アクセス要件に基づき、次の権限を定義できます:
権限 | 説明 |
---|---|
list:articles | システム内のすべての記事一覧を閲覧 |
read:articles | 任意の記事の全文を閲覧 |
create:articles | 新規記事の作成 |
update:articles | 任意の記事の編集 |
delete:articles | 任意の記事の削除 |
publish:articles | 公開ステータスの変更 |
これらの権限は、自分が所有していないリソースにアクセスする場合のみ必要です。記事の所有者は自動的に:
- 自分の記事を閲覧(
read:articles
不要) - 自分の記事を編集(
update:articles
不要) - 自分の記事を削除(
delete:articles
不要)
効果的なロールの構築
API と権限が定義できたので、これらの権限を論理的にまとめたロールを作成します:
権限/ロール | 👑 管理者 (Admin) | 📝 パブリッシャー (Publisher) | ✍️ 著者 (Author) |
---|---|---|---|
説明 | 全コンテンツ管理のためのシステム全体アクセス | すべての記事の閲覧と公開ステータス管理が可能 | システム内で新規記事作成が可能 |
list:articles | ✅ | ✅ | ❌ |
read:articles | ✅ | ✅ | ❌ |
create:articles | ✅ | ❌ | ✅ |
update:articles | ✅ | ❌ | ❌ |
delete:articles | ✅ | ❌ | ❌ |
publish:articles | ✅ | ✅ | ❌ |
注:著者はロール権限に関係なく、自分の記事に対して read/update/delete 権限を自動的に持ちます。
各ロールは明確な責任を持って設計されています:
- 管理者 (Admin):CMS 全体の完全なコントロール(すべての記事操作を含む)
- パブリッシャー (Publisher):コンテンツのレビューと公開管理に特化
- 著者 (Author):コンテンツ作成に特化
このロール構成により、役割ごとの責任範囲が明確になります:
- 著者はコンテンツ作成に集中
- パブリッシャーはコンテンツの品質と公開を管理
- 管理者はシステム全体を管理
Logto での RBAC 設定
始める前に、 Logto Cloud でアカウントを作成する必要があります。または、 Logto OSS バージョン を使ってセルフホスト型 Logto インスタンスを利用することもできます。
このチュートリアルでは、簡単のため Logto Cloud を使用します。
アプリケーションのセットアップ
- Logto コンソールの「アプリケーション」で新しい React アプリケーションを作成
- アプリケーション名:Content Management System
- アプリケーションタイプ:従来型 Web アプリケーション
- リダイレクト URI:http://localhost:5173/callback
API リソースと権限の設定
- Logto コンソールの「API リソース」で新しい API リソースを作成
- API 名:CMS API
- API 識別子:https://api.cms.com
- API リソースに権限を追加
list:articles
read:articles
create:articles
update:articles
publish:articles
delete:articles
ロールの作成
Logto コンソールの「ロール」で、CMS 用に次のロールを作成します
- 管理者 (Admin)
- すべての権限を付与
- パブリッシャー (Publisher)
read:articles
,list:articles
,publish:articles
を付与
- 著者 (Author)
create:articles
を付与
ユーザーへのロール割り当て
Logto コンソールの「ユーザー管理」セクションでユーザーを作成します。
ユーザー詳細の「ロール」タブで、ユーザーにロールを割り当てられます。
本例では、次の 3 ユーザーを作成し、それぞれにロールを割り当てます:
- Alex:管理者 (Admin)
- Bob:パブリッシャー (Publisher)
- Charlie:著者 (Author)
デモ目的のため、これらのリソースや設定は Logto コンソールから作成しています。実際のプロジェクトでは、Logto が提供する Management API を使ってプログラム的に作成・設定できます。
フロントエンドと Logto RBAC の連携
Logto で RBAC の設定ができたので、フロントエンドに組み込んでいきます。
まず、 Logto クイックスタート に従ってアプリケーションへ Logto を組み込みます。
本例では React を使用しています。
アプリケーションに Logto を組み込んだら、RBAC 設定を追加します。
// frontend/src/App.tsx
const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // Logto コンソールで作成したアプリ ID
endpoint: LOGTO_ENDPOINT, // Logto コンソールで作成したエンドポイント
resources: [API_RESOURCE], // Logto コンソールで作成した API リソース識別子(例:https://api.cms.com)
// フロントエンドで API リソースからリクエストしたいすべてのスコープ
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};
すでにサインインしている場合は、一度サインアウトして再度サインインすることで変更が反映されます。
ユーザーが Logto でサインインし、上記 API リソースのアクセス トークンをリクエストすると、Logto はユーザーのロールに関連するスコープ(権限)をアクセス トークンに追加します。
useLogto
フックの getAccessTokenClaims
を使って、アクセス トークンからスコープを取得できます。
// frontend/src/hooks/use-user-data.ts
import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';
export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();
useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};
fetchScopes();
}, [getAccessTokenClaims]);
return { userId, userScopes };
};
userScopes
を使って、ユーザーがリソースにアクセスする権限を持っているかどうかを判定できます。
// frontend/src/pages/Dashboard.tsx
const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...
return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
)}
</div>
);
};
バックエンドと Logto RBAC の連携
次に、Logto RBAC をバックエンドに組み込みます。
バックエンド認可 (Authorization) ミドルウェア
まず、バックエンドでユーザー権限のチェック、ログイン状態の確認、特定 API へのアクセス権限の有無を判定するミドルウェアを追加します。
// backend/src/middleware/auth.js
const { createRemoteJWKSet, jwtVerify } = require('jose');
const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';
if (!authorization) {
throw new Error('Authorization header missing');
}
if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}
return authorization.slice(bearerTokenIdentifier.length + 1);
};
const hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});
return payload;
};
const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// トークンを抽出
const token = getTokenFromHeader(req.headers);
// トークンを検証
const payload = await verifyJwt(token);
// ユーザー情報をリクエストに追加
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};
// 必要なスコープを検証
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
hasScopes,
};
このミドルウェアでは、フロントエンドリクエストに有効なアクセス トークンが含まれているか、アクセス トークンのオーディエンスが Logto コンソールで作成した API リソースと一致しているかを検証しています。
API リソースを検証する理由は、この API リソースが実際に CMS バックエンドのリソースを表しており、すべての CMS 権限がこの API リソースに紐付いているためです。
この API リソースが Logto で CMS リソースを表しているため、フロントエンドコードではバックエンド API リクエスト時に対応するアクセス トークンを付与します:
// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();
return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// API リソース用のアクセス トークンを取得
const token = await getAccessToken(API_RESOURCE);
if (!token) {
throw new ApiRequestError('Failed to get access token');
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// アクセス トークンをリクエストヘッダーに追加
Authorization: `Bearer ${token}`,
...options.headers,
},
});
// ... handle response
return await response.json();
} catch (error) {
// ... error handling
}
},
[getAccessToken]
);
};
これで requireAuth
ミドルウェアを使って API エンドポイントを保護できます。
API エンドポイントの保護
特定の権限を持つユーザーだけがアクセスできる API には、ミドルウェアで直接制限を追加できます。たとえば、記事作成 API は create:articles
権限を持つユーザーのみアクセス可能です:
// backend/src/routes/articles.js
const { requireAuth } = require('../middleware/auth');
router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});
権限とリソース所有権の両方をチェックする必要がある API では、hasScopes
関数を利用します。たとえば、記事一覧 API では list:articles
権限を持つユーザーはすべての記事にアクセスでき、著者は自分が作成した記事のみアクセスできます:
// backend/src/routes/articles.js
const { requireAuth, hasScopes } = require('../middleware/auth');
router.get('/articles', requireAuth(), async (req, res) => {
try {
// list:articles スコープを持つ場合は全記事を返す
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}
// それ以外は自分の記事のみ返す
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});
これで RBAC 実装が完了しました。 完全なソースコード で全体の実装を確認できます。
CMS RBAC 実装のテスト
作成した 3 ユーザーを使って、CMS RBAC 実装をテストしてみましょう。
「ユーザー管理」で作成したユーザーの認証情報でサインインできない場合は、まず適切なサインイン方法を有効にする必要があります。Logto コンソールの「サインイン体験」で、メール+パスワードやユーザー名+パスワードなど、希望の認証方法を有効にしてください。
まず、Alex と Charles でそれぞれサインインし、記事を作成してみます。
Alex は管理者 (Admin) ロールを持っているため、記事の作成・削除・編集・公開・すべての記事の閲覧が可能です。
Charles は著者 (Author) ロールのため、自分の記事のみ作成・閲覧・編集・削除が可能です。
Bob はパブリッシャー (Publisher) ロールで、すべての記事の閲覧・公開はできますが、作成・編集・削除はできません。
まとめ
おめでとうございます!アプリケーションに堅牢な RBAC システムを実装する方法を学びました。
より複雑なシナリオ(マルチテナントアプリケーションの構築など)にも対応できるよう、Logto では包括的な組織 (Organization) サポートを提供しています。組織単位のアクセス制御の実装については、 マルチテナント SaaS アプリケーション構築ガイド:設計から実装まで をご覧ください。
Happy coding! 🚀