メインコンテンツまでスキップ

実践的な 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 つの側面を考慮します:

  1. リソース所有権 - ユーザーはこのリソースの所有者か?
  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 を使用します。

アプリケーションのセットアップ

  1. Logto コンソールの「アプリケーション」から新しい React アプリケーションを作成
    • アプリケーション名:Content Management System
    • アプリケーションタイプ:従来型 Web アプリケーション
    • リダイレクト URI: http://localhost:5173/callback

CMS React application

API リソースと権限の設定

  1. Logto コンソールの「API リソース」から新しい API リソースを作成
    • API 名:CMS API
    • API 識別子: https://api.cms.com
    • API リソースに権限を追加
      • list:articles
      • read:articles
      • create:articles
      • update:articles
      • publish:articles
      • delete:articles

CMS API resource details

ロールの作成

Logto コンソールの「ロール」から、CMS 用に次のロールを作成します

  • 管理者 (Admin)
    • すべての権限を付与
  • パブリッシャー (Publisher)
    • read:articles, list:articles, publish:articles を付与
  • 著者 (Author)
    • create:articles を付与

Admin role

Publisher role

Author role

ユーザーへのロール割り当て

Logto コンソールの「ユーザー管理」セクションでユーザーを作成します。

ユーザー詳細の「ロール」タブで、ユーザーにロールを割り当てられます。

本例では、次のロールで 3 人のユーザーを作成します:

  • Alex:管理者 (Admin)
  • Bob:パブリッシャー (Publisher)
  • Charlie:著者 (Author)

User management

User details - Alex

注記:

デモのため、これらのリソースや設定は Logto コンソールから作成しています。実際のプロジェクトでは、Logto が提供する Management API を使ってプログラム的にリソースや設定を作成できます。

フロントエンドと Logto RBAC の連携

Logto で RBAC の設定ができたので、フロントエンドに組み込んでいきます。

まず、 Logto クイックスタート に従ってアプリケーションへ Logto を組み込みます。

本例では React を使って説明します。

アプリケーションに Logto をセットアップしたら、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 リソースのアクセス トークン (Access token) をリクエストすると、Logto はユーザーのロールに関連するスコープ(権限)をアクセス トークン (Access token) に追加します。

useLogto フックの getAccessTokenClaims を使って、アクセス トークン (Access token) からスコープを取得できます。

// 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) ミドルウェア

まず、バックエンドでユーザー権限のチェック、ログイン済みかどうかの確認、必要な権限を持っているかの判定を行うミドルウェアを追加します。

// 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,
};

このミドルウェアでは、フロントエンドからのリクエストに有効なアクセス トークン (Access token) が含まれているかを検証し、アクセス トークン (Access token) のオーディエンスが Logto コンソールで作成した API リソースと一致するかを確認しています。

API リソースを検証する理由は、API リソースが実際に CMS バックエンドのリソースを表しており、すべての CMS 権限がこの API リソースに紐付いているためです。

この API リソースは Logto 上で CMS リソースを表すため、フロントエンドのコードでは、バックエンドへの API リクエスト時に対応するアクセス トークン (Access token) をリクエストヘッダーに含めます:

// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();

return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// API リソース用のアクセス トークン (Access token) を取得
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',
// アクセス トークン (Access token) をリクエストヘッダーに追加
Authorization: `Bearer ${token}`,
...options.headers,
},
});

// ... レスポンス処理

return await response.json();
} catch (error) {
// ... エラー処理
}
},
[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) ロールを持っているため、記事の作成・削除・編集・公開・閲覧がすべて可能です。

CMS dashboard - Alex

Charles は著者 (Author) ロールのため、自分の記事のみ作成・閲覧・編集・削除が可能です。

CMS dashboard - Charles - Article list

Bob はパブリッシャー (Publisher) ロールで、すべての記事の閲覧と公開はできますが、作成・編集・削除はできません。

CMS dashboard - Bob

まとめ

おめでとうございます!アプリケーションに堅牢な RBAC システムを実装する方法を学びました。

より複雑なシナリオ、たとえばマルチテナントアプリケーションの構築などには、Logto の包括的な組織 (Organization) サポートが役立ちます。組織単位でのアクセス制御の実装については、 マルチテナント SaaS アプリケーション構築:設計から実装までの完全ガイド をご覧ください。

Happy coding! 🚀