マルチテナント SaaS アプリケーションの構築:設計から実装までの完全ガイド
Notion や Slack、Figma のようなアプリはどのように作られているのでしょうか?これらのマルチテナント SaaS アプリケーションは使うのは簡単そうに見えますが、自分で作るとなると話は別です。
私が初めてこのような複雑なものを作ろうと考えたとき、頭が爆発しそうになりました:
- ユーザーは複数のサインインオプション(メール、Google、GitHub)が必要
- 各ユーザーは複数の組織 (Organization) を作成・所属できる
- 各組織 (Organization) 内で異なる権限レベルが必要
- 特定のメールドメインで自動参加するエンタープライズ組織
- 機密操作のための MFA 必須
- その他いろいろ...
「ボス、2 週間後にプロダクト設計の話をしましょう。今は泥沼にはまっています。」
でも実際に手を動かしてみると、思ったほど大変ではないことに気づきました。
これらすべての機能を、驚くほど少ない労力で実装できました!


どのようにしてこのようなシステムをゼロから設計・実装するのか、具体的に紹介します。2025 年の今、モダンなツールと正しいアーキテクチャを使えば、驚くほどシンプルに実現できます。
完全なソースコードは Github リポジトリ で公開しています。さっそく始めましょう!
今回は DocuMind という AI ドキュメント SaaS プロダクトを例にします。
DocuMind は、個人ユーザー・中小企業・エンタープライズをサポートするマルチテナントモデルで設計された AI ドキュメント SaaS プロダクトです。
このプラットフォームは、組織 (Organization) 内での自動要約生成、重要ポイント抽出、インテリジェントなコンテンツ推薦など、強力な AI ドキュメント管理機能を提供します。
SaaS の認証 (Authentication)・認可 (Authorization) に必要な機能とは?
まずは必要な要件を整理しましょう。どんな機能が必要でしょうか?
マルチテナントアーキテクチャ
マルチテナントアーキテクチャを実現するには、組織 (Organization) というエンティティ層が必要です。1 つのユーザープールで複数のワークスペースにアクセスできるイメージです。各組織 (Organization) がワークスペースを表し、ユーザーは 1 つのアイデンティティで、割り当てられたロールに応じて異なるワークスペース(組織)にアクセスします。
これは認証 (Authentication) プロバイダーで広く使われている機能です。アイデンティティ管理システムにおける組織 (Organization) は、SaaS アプリのワークスペースやプロジェクト、テナントに相当します。
メンバーシップ
メンバーとは、組織 (Organization) 内でのアイデンティティの所属状態を示す一時的な概念です。
たとえば、Sarah さんがメール sarah@gmail.com でアプリに登録したとします。Sarah さんは異なるワークスペースに所属できます。Sarah さんが Workspace A には所属しているが Workspace B には所属していない場合、Sarah さんは Workspace A のメンバーですが、Workspace B のメンバーではありません。
ロールと権限設計
マルチテナントアーキテクチャでは、ユーザーはテナントリソースにアクセスするための特定の ロール (Role) と 権限 (Permission) が必要です。
権限 (Permission) とは、read: order
や write: order
のような具体的な操作を定義する詳細なアクセス制御です。どのリソースに対してどんな操作ができるかを決定します。
ロール (Role) とは、マルチテナント環境でメンバーに割り当てられる権限 (Permission) の集合です。
これらのロールと権限 (Permission) を定義し、ユーザーにロールを割り当てる必要があります。場合によっては自動化も必要です。たとえば:
- 組織 (Organization) に参加したユーザーは自動的に member ロールを取得
- 最初にワークスペースを作成したユーザーは自動的に admin ロールを取得
サインアップ・ログインフロー
ユーザーフレンドリーかつ安全な登録・認証 (Authentication) プロセスを実現しましょう。基本的なサインイン・サインアップオプションを含みます:
- メール&パスワードサインイン:従来のメール&パスワードによるログイン
- パスワードレスサインイン:メール認証コードによる簡単・安全なアクセス
- アカウント管理:メールやパスワードなどを更新できるアカウントセンター
- ソーシャルサインイン:Google や GitHub などによるクイックログイン
- 多要素認証 (MFA):Duo などの認証アプリによるログインでセキュリティ強化
テナント作成と招待
マルチテナント SaaS アプリでは、ユーザーフローの大きな違いとして、テナント作成やメンバー招待のサポートが必要です。このプロセスはプロダクトのアクティベーションや成長に重要な役割を果たすため、慎重な設計と実装が求められます。
考慮すべき典型的な利用フローをいくつか挙げます:
ユーザータイプ | エントリーポイント |
---|---|
新規アカウント | サインイン・サインアップページから新規テナント作成 |
既存アカウント | プロダクト内で別のテナントを作成 |
既存アカウントが新しいテナント招待を受け取った | サインイン・サインアップページから参加 |
既存アカウントが新しいテナント招待を受け取った | 招待メールから参加 |
新規アカウントが新しいテナント招待を受け取った | サインイン・サインアップページから参加 |
新規アカウントが新しいテナント招待を受け取った | 招待メールから参加 |
ほとんどの SaaS アプリで見られる一般的なシナリオです。これらを参考にプロダクト・デザインチームで独自のフローを設計してください。






技術アーキテクチャとシステム設計
すべてのプロダクト要件が整理できたら、実装に進みましょう。
認証 (Authentication) 戦略の定義
認証 (Authentication) は難しそうに見えます。ユーザーは次のようなものが必要です:
- メール&パスワードでのサインアップ/ログイン
- Google/Github でワンクリックサインイン
- パスワードを忘れたときのリセット
- エンタープライズ顧客向けのチーム全体ログイン
- ...
これらの基本機能だけでも数週間かかることもあります。
でも今は、これらを自分で作る必要はありません!
モダンな認証 (Authentication) プロバイダー(今回は Logto を選びます)が、これらすべての機能をパッケージ化してくれています。認証 (Authentication) フローはとてもシンプルです:
数週間かかる開発が 15 分のセットアップに! Logto が複雑なフローをすべて処理してくれます。統合手順は後ほど実装セクションで解説します。今は DocuMind のコア機能開発に集中できます!
マルチテナントアーキテクチャの構築
組織 (Organization) システムにより、ユーザーは複数の組織 (Organization) を作成・参加できます。コアとなる関係性を理解しましょう:
このシステムでは、各ユーザーは複数の組織 (Organization) に所属でき、各組織 (Organization) には複数のメンバーが所属できます。
マルチテナントアプリでのアクセス制御の有効化
ロールベースのアクセス制御 (RBAC) は、マルチテナント SaaS アプリケーションのセキュリティとスケーラビリティを確保するために重要です。
マルチテナントアプリでは、権限 (Permission) とロール (Role) の設計は通常一貫しています。たとえば、複数のワークスペースでは管理者ロールとメンバーロールが一般的です。Logto の組織 (Organization) レベルのロールベースアクセス制御設計は次の通りです:
- 統一された権限 (Permission) 定義:権限 (Permission) はシステムレベルで定義され、すべての組織 (Organization) で一貫して適用されるため、管理が容易で一貫性のある権限管理が可能
- 組織テンプレート:あらかじめ定義されたロール (Role) と権限 (Permission) の組み合わせをテンプレート化し、組織 (Organization) の初期化を簡素化
権限 (Permission) の関係性は次のようになります:
各ユーザーは各組織 (Organization) ごとに独自のロール (Role) を持つ必要があるため、ロール (Role) と組織 (Organization) の関係はユーザーごとに割り当てられたロール (Role) を反映する必要があります:
これで組織 (Organization) システムとアクセス制御システムの設計ができたので、いよいよプロダクト開発に取りかかれます!
技術スタック
初心者にも優しく、移植性の高いスタックを選びました:
- フロントエンド:React(Vue/Angular/Svelte への移行も簡単)
- バックエンド:Express(シンプルで直感的な API)
なぜフロントエンドとバックエンドを分離するのか?それはアーキテクチャが明確で、学びやすく、スタックの切り替えも簡単だからです。認証 (Authentication) プロバイダーには Logto を例に使います。
このガイドのパターンは、どんなフロントエンド・バックエンド・認証 (Authentication) システムでも応用できます。
アプリに基本的な認証 (Authentication) フローを追加
これは最も簡単なステップです。Logto をプロジェクトに統合するだけです。あとは Logto コンソールでログイン/登録方法を設定できます。
アプリに Logto をインストール
まず Logto Cloud にログインします。アカウントがなければ無料で登録できます。テスト用に Development Tenant を作成しましょう。
テナントコンソールで左側の「アプリケーション」ボタンをクリックし、React を選択してアプリケーション構築を始めます。
ページのガイドに従えば、Logto の統合は約 5 分で完了します!
私の統合コード例はこちらです:
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
};
function App() {
return (
<LogtoProvider config={config}>
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100">
<Routes>
{/* Logto からのユーザーログインリダイレクトを処理 */}
<Route path="/callback" element={<Callback />} />
<Route path="/*" element={<AppContent />} />
</Routes>
</div>
</LogtoProvider>
);
}
function AppContent() {
const { isAuthenticated } = useLogto();
if (!isAuthenticated) {
// 未認証ユーザー向けランディングページ
return <Landing />;
}
// 認証済みユーザー向けメインアプリ
return (
<Routes>
{/* ダッシュボードで利用可能なすべての組織 (Organization) を表示 */}
<Route path="/" element={<Dashboard />} />
{/* ダッシュボードで組織 (Organization) をクリックした後のページ */}
<Route path="/:orgId" element={<Organization />} />
</Routes>
);
}
便利な小技:ログインページには「Sign in」と「Register」ボタンがあります。Register ボタンは Logto の登録ページに直接遷移します。これは Logto の first screen 機能で実現しています。ユーザーが最初にどの認証 (Authentication) ステップを見るかを決定できます。
新規ユーザーが多い場合は、デフォルトで登録ページを表示するのもおすすめです。
function LandingPage() {
const { signIn } = useLogto();
return (
<div className="landing-container">
<div className="auth-buttons">
<button
className="sign-in-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
});
}}
>
Sign In
</button>
<button
className="register-button"
onClick={() => {
signIn({
redirectUri: '<YOUR_APP_CALLBACK_URL>',
firstScreen: 'register',
});
}}
>
Register
</button>
</div>
</div>
);
}
ログインをクリックすると Logto のログインページに遷移します。ログイン(または登録)に成功すると、おめでとうございます!アプリに最初のユーザー(自分自身)が誕生しました!
ユーザーをサインアウトしたいときは、useLogto
フックの signOut
関数を呼び出します。
function SignOutButton() {
const { signOut } = useLogto();
return <button onClick={() => signOut('<YOUR_POST_LOGOUT_REDIRECT_URL>')}>Sign Out</button>;
}
サインイン・サインアップ方法のカスタマイズ
Logto コンソールの左メニューで「サインイン体験」をクリックし、「サインアップとサインイン」タブを選択します。 このページで Logto のログイン/登録方法を設定できます。
サインインフローはこのようになります:

多要素認証 (MFA) の有効化
Logto なら MFA の有効化も簡単です。Logto コンソールで「多要素認証」ボタンをクリックし、Multi-factor authentication ページで有効化するだけです。
MFA フローはこのようになります:


とてもシンプルですね!わずか数分で複雑なユーザー認証 (Authentication) システムを構築できました!
マルチテナント組織 (Organization) 体験の追加
これで最初のユーザーができました!しかし、このユーザーはまだどの組織 (Organization) にも所属しておらず、組織 (Organization) も作成されていません。
Logto はマルチテナンシーを標準サポートしています。Logto で任意の数の組織 (Organization) を作成できます。各組織 (Organization) には複数のメンバーが所属できます。
各ユーザーは Logto から自分の組織 (Organization) 情報を取得できます。これによりマルチテナンシーが実現できます。
ユーザーの組織 (Organization) 情報を取得
Logto からユーザーの組織 (Organization) 情報を取得するには、次の 2 ステップを実施します:
Logto Config で組織 (Organization) 情報へのアクセスを宣言します。適切な scopes
と resources
を設定します。
import { UserScope, ReservedResource } from "@logto/react";
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations], // 値: "urn:logto:scope:organizations"
resources: [ReservedResource.Organization], // 値: "urn:logto:resource:organizations"
};
Logto の fetchUserInfo
メソッドでユーザー情報(組織 (Organization) データ含む)を取得します。
function Dashboard() {
// ユーザー情報取得
const { fetchUserInfo } = useLogto();
const [organizations, setOrganizations] = useState<OrganizationData[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadOrganizations = async () => {
try {
setLoading(true);
// ユーザー情報取得
const userInfo = await fetchUserInfo();
// ユーザーの組織 (Organization) 情報取得
const organizationData = userInfo?.organization_data || [];
setOrganizations(organizationData);
} catch (error) {
console.error('Failed to fetch organizations:', error);
} finally {
setLoading(false);
}
};
loadOrganizations();
}, [fetchUserInfo]);
if (loading) {
return <div>Loading...</div>;
}
if (organizations.length === 0) {
return <div>まだどの組織 (Organization) にも所属していません</div>;
}
return <div>Organizations: {organizations.map(org => org.name).join(', ')}</div>;
}
これらの手順を終えたら、一度サインアウトして再度サインインしてください。リクエストするスコープやリソースを変更したためです。
現時点では、まだ組織 (Organization) を作成していませんし、ユーザーもどの組織 (Organization) にも参加していません。ダッシュボードには「まだ組織 (Organization) がありません」と表示されます。
次に、ユーザーのために組織 (Organization) を作成し、そこに追加します。
Logto のおかげで、複雑な組織 (Organization) 関係を自作する必要はありません。Logto で組織 (Organization) を作成し、ユーザーを追加するだけです。Logto が複雑な部分をすべて処理してくれます。組織 (Organization) を作成する方法は 2 つあります:
- Logto コンソールで手動作成
- Logto Management API を使って作成(ユーザー自身がワークスペースを作成できる SaaS フロー設計時など)
Logto コンソールで組織 (Organization) を作成
Logto コンソール左側の「組織 (Organizations)」メニューボタンをクリックし、組織 (Organization) を作成します。
これで最初の組織 (Organization) ができました。
次に、この組織 (Organization) にユーザーを追加しましょう。
組織 (Organization) 詳細ページに移動し、メンバータブに切り替えます。「+ メンバー追加」ボタンをクリックし、左側リストからログインユーザーを選択します。右下の「メンバー追加」ボタンをクリックすれば、ユーザーが組織 (Organization) に追加されます。
アプリページをリロードすると、ユーザーが組織 (Organization) に所属していることが確認できます!
セルフサーブでの組織 (Organization) 作成体験の実装
コンソールで組織 (Organization) を作成するだけでは不十分です。SaaS アプリには、エンドユーザーが自分でワークスペースを簡単に作成・管理できるフローが必要です。この機能を実装するには Logto Management API を使います。
API 通信のセットアップ方法は Management API との連携 ドキュメントを参照してください。
組織 (Organization) 認証 (Authentication) インタラクションフローの理解
組織 (Organization) 作成フローを例に、プロセスを見てみましょう:
このフローには 2 つの重要な認証 (Authentication) 要件があります:
- バックエンドサービス API の保護:
- フロントエンドからバックエンドサービス API へのアクセスには認証 (Authentication) が必要
- API エンドポイントは Logto アクセストークンの検証で保護
- 認証済みユーザーのみサービスにアクセス可能
- Logto Management API へのアクセス:
- バックエンドサービスが Logto Management API を安全に呼び出す必要あり
- Management API との連携 ガイドに従ってセットアップ
- マシン間通信 (M2M) 認証 (Authentication) でアクセス認証情報を取得
バックエンド API の保護
まず、バックエンドサービスで組織 (Organization) 作成用の API エンドポイントを作成します。
app.post('/organizations', async (req, res) => {
// Logto Management API を使った実装
// ...
});
バックエンドサービス API は認証済みユーザーのみ許可します。Logto で API を保護し、現在のユーザー情報(ユーザー ID など)も取得する必要があります。
Logto の概念(および OAuth 2.0)では、バックエンドサービスはリソースサーバーとして動作します。ユーザーはフロントエンドからアクセストークンを使って DocuMind リソースサーバーにアクセスし、リソースサーバーはこのトークンを検証します。有効ならリソースを返します。
API リソースを作成してバックエンドサービスを表現しましょう。
Logto コンソールで:
- 右側の「API リソース」ボタンをクリック
- 「API リソース作成」をクリックし、ポップアップで Express を選択
- API 名に「DocuMind API」、API 識別子に「https://api.documind.com」を入力
- 作成をクリック
この API 識別子 URL は Logto 内での一意識別子であり、実際のバックエンドサービス URL とは関係ありません。
API リソースの使い方チュートリアルが表示されます。そちらを参照しても、この後の手順に従っても OK です。
requireAuth
ミドルウェアを作成し、POST /organizations エンドポイントを保護します。
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 requireAuth = (resource) => {
if (!resource) {
throw new Error('Resource parameter is required for authentication');
}
return async (req, res, next) => {
try {
// トークン抽出
const token = getTokenFromHeader(req.headers);
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: resource,
}
);
// ユーザー情報をリクエストに追加
req.user = {
id: payload.sub,
};
next();
} catch (error) {
console.error('Auth error:', error);
res.status(401).json({ error: 'Unauthorized' });
}
};
};
module.exports = {
requireAuth,
};
このミドルウェアを使うには、次の環境変数が必要です:
- LOGTO_JWKS_URL
- LOGTO_ISSUER
これらは Logto テナントの OpenID Configuration エンドポイントから取得します。https://<your-tenant-id>.logto.app/oidc/.well-known/openid-configuration
にアクセスし、返却される JSON から必要な情報を確認してください:
{
"jwks_uri": "<https://tenant-id.logto.app/oidc/jwks>",
"issuer": "<https://tenant-id.logto.app/oidc>"
}
requireAuth
ミドルウェアを POST /organizations エンドポイントで使いましょう。
app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
// 組織 (Organization) 作成ロジック
// ...
});
これで POST /organizations エンドポイントが保護され、Logto の有効なアクセストークンを持つユーザーのみアクセス可能になります。
フロントエンドで Logto からトークンを取得し、このトークンでバックエンドサービス API を呼び出せます。ミドルウェアでユーザー ID も取得できるので、組織 (Organization) 追加時に便利です。
フロントエンドコードでは、この API リソースを Logto config の resources 配列に追加します。
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // 新規作成した API リソース識別子
};
Logto config を更新したら、ユーザーは再度ログインが必要です。
ダッシュボードで組織 (Organization) 作成時に Logto アクセストークンを取得し、このトークンでバックエンドサービス API を呼び出します。
// "DocuMind API" 用アクセストークン取得
const token = await getAccessToken('<https://api.documind.com>');
// トークン付きでバックエンドサービス API にアクセス
const response = await fetch('<http://localhost:3000/organizations>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: 'Organization A',
description: 'Organization A description',
}),
});
これで DocuMind バックエンドサービス API に正しくアクセスできます。
Logto Management API の呼び出し
Logto Management API を使って組織 (Organization) 作成を実装しましょう。
フロントエンドからバックエンドサービスへのリクエストと同様に、バックエンドサービスから Logto へのリクエストにもアクセストークンが必要です。
Logto では、マシン間通信 (M2M) 認証 (Authentication) でアクセストークンを取得します。Management API との連携 を参照してください。
Logto コンソールのアプリケーションページでマシン間通信 (M2M) アプリケーションを作成し、「Logto Management API アクセス」ロールを割り当てます。トークンエンドポイント、App ID、App Secret をコピーしておきます。
この M2M アプリケーションで Logto Management API のアクセストークンを取得できます。
async function fetchLogtoManagementApiAccessToken() {
const response = await fetch(process.env.LOGTO_MANAGEMENT_API_TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${process.env.LOGTO_MANAGEMENT_API_APPLICATION_ID}:${process.env.LOGTO_MANAGEMENT_API_APPLICATION_SECRET}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
resource: process.env.LOGTO_MANAGEMENT_API_RESOURCE,
scope: 'all',
}).toString(),
});
const data = await response.json();
return data.access_token;
}
このアクセストークンで Logto Management API を呼び出します。
利用する Management API は次の通りです:
POST /api/organizations
: 組織 (Organization) 作成(API リファレンス 参照)POST /api/organizations/{id}/users
: 組織 (Organization) にユーザー追加(API リファレンス 参照)
app.post('/organizations', requireAuth('<https://api.documind.com>'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// Logto で組織 (Organization) 作成&ユーザー追加
const response = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
name: req.body.name,
description: req.body.description,
}),
});
const createdOrganization = await response.json();
await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});
res.json({ data: createdOrganization });
});
これで Logto Management API を使った組織 (Organization) 作成とユーザー追加が実装できました。
ダッシュボードでこの機能をテストしましょう。
「Create Organization」をクリック
作成成功!
次は組織 (Organization) へのユーザー招待機能ですが、本チュートリアルでは割愛します。Management API の使い方はすでに説明済みなので、テナント作成と招待 をプロダクト設計の参考にし、How we implement user collaboration within a multi-tenant app を参考に簡単に実装できます。
マルチテナントアプリへのアクセス制御の実装
次は組織 (Organization) のアクセス制御です。
実現したいこと:
- ユーザーは自分の組織 (Organization) のリソースのみアクセス可能:Logto の
組織トークン (Organization token)
で実現 - ユーザーは組織 (Organization) 内で特定のロール (Role)(異なる権限 (Permission) を含む)を持ち、認可された操作のみ実行可能:Logto の組織テンプレート機能で実現
これらの機能の実装方法を見ていきましょう。
Logto 組織トークン (Organization token) の利用
先ほどの Logto アクセストークンと同様に、Logto は特定リソースに対応するアクセストークンを発行し、ユーザーはこのトークンでバックエンドサービスの保護リソースにアクセスします。同様に、Logto は特定の組織 (Organization) に対応する組織トークン (Organization token) を発行し、ユーザーはこのトークンで組織 (Organization) の保護リソースにアクセスします。
フロントエンドアプリでは、Logto の getOrganizationToken
メソッドで特定組織 (Organization) 用のトークンを取得できます。
const { getOrganizationToken } = useLogto();
const organizationToken = await getOrganizationToken(organizationId);
ここで organizationId
はユーザーが所属する組織 (Organization) の id です。
getOrganization
や組織 (Organization) 関連機能を使う前に、Logto config に urn:logto:scope:organizations
スコープと urn:logto:resource:organization
リソースが含まれていることを確認してください(すでに宣言済みなので再掲しません)。
組織 (Organization) ページでは、この組織トークン (Organization token) で組織 (Organization) 内のドキュメントを取得します。
function OrganizationPage() {
const { organizationId } = useParams();
const navigate = useNavigate();
const { signOut, getOrganizationToken } = useLogto();
const [error, setError] = useState<Error | null>(null);
const [documents, setDocuments] = useState([]);
const fetchDocuments = useCallback(async () => {
if (!organizationId) return;
try {
const organizationToken = await getOrganizationToken(organizationId);
const response = await fetch(`http://localhost:3000/documents`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${organizationToken}`,
},
});
const documents = await response.json();
setDocuments(documents);
} catch (error: unknown) {
if (error instanceof Error) {
setError(error);
} else {
setError(new Error(String(error)));
}
}
},[getOrganizationToken, organizationId]);
useEffect(() => {
void fetchDocuments();
}, [fetchDocuments]);
if (error) {
return <div>Error: {error.message}</div>;
}
return <div>
<h1>Organization Documents</h1>
<ul>
{documents.map((document) => (
<li key={document.id}>{document.name}</li>
))}
</ul>
</div>
}
この実装で重要なポイントは 2 つあります:
getOrganizationToken
に渡すorganizationId
が現在のユーザーの所属組織 (Organization) でない場合、トークンは取得できません。これによりユーザーは自分の組織 (Organization) のみアクセス可能となります。- 組織 (Organization) リソースへのリクエストにはアクセストークンではなく組織トークン (Organization token) を使います。組織 (Organization) に属するリソースにはユーザー権限 (Permission) ではなく組織 (Organization) 権限 (Permission) 制御を適用したいためです(この後
GET /documents
API 実装でより理解できます)。
次に、バックエンドサービスで GET /documents
API を作成します。POST /organizations
API を API リソースで保護したのと同様に、組織 (Organization) 固有のリソースインジケーターで GET /documents
API を保護します。
まず、組織 (Organization) リソースを保護する requireOrganizationAccess
ミドルウェアを作成します。
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 extractOrganizationId = (aud) => {
if (!aud || typeof aud !== 'string' || !aud.startsWith('urn:logto:organization:')) {
throw new Error('Invalid organization token');
}
return aud.replace('urn:logto:organization:', '');
};
const decodeJwtPayload = (token) => {
try {
const [, payloadBase64] = token.split('.');
if (!payloadBase64) {
throw new Error('Invalid token format');
}
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8');
return JSON.parse(payloadJson);
} catch (error) {
throw new Error('Failed to decode token payload');
}
};
const requireOrganizationAccess = () => {
return async (req, res, next) => {
try {
// トークン抽出
const token = getTokenFromHeader(req.headers);
// トークンから audience を動的取得
const { aud } = decodeJwtPayload(token);
if (!aud) {
throw new Error('Missing audience in token');
}
// audience でトークン検証
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);
// audience クレームから組織 (Organization) ID 抽出
const organizationId = extractOrganizationId(payload.aud);
// 組織 (Organization) 情報をリクエストに追加
req.user = {
id: payload.sub,
organizationId,
};
next();
} catch (error) {
console.error('Organization auth error:', error);
res.status(401).json({ error: 'Unauthorized - Invalid organization access' });
}
};
};
requireOrganizationAccess
ミドルウェアで GET /documents
API を保護します。
app.get('/documents', requireOrganizationAccess(), async (req, res) => {
// req.user から現在のユーザー ID と組織 (Organization) ID を取得
console.log('userId', req.user.id);
console.log('organizationId', req.user.organizationId);
// organizationId でデータベースからドキュメント取得
// ....
const documents = await getDocumentsByOrganizationId(req.user.organizationId);
res.json(documents);
});
このようにして、組織トークン (Organization token) で組織 (Organization) リソースにアクセスできるようになりました。バックエンドサービスでは組織 (Organization) ID で該当リソースをデータベースから取得できます。
組織 (Organization) 間でデータ分離が必要なソフトウェアもあります。さらに詳しい議論や実装例は Multi-tenancy implementation with PostgreSQL: Learn through a simple real-world example を参照してください。
組織 (Organization) レベルのロールベースアクセス制御設計の実装
組織トークン (Organization token) で組織 (Organization) リソースにアクセスできるようになりました。次は RBAC を使って組織 (Organization) 内のユーザー権限 (Permission) 制御を実装します。
DocuMind には Admin と Collaborator の 2 つのロール (Role) があると仮定します。
Admin はドキュメントの作成・閲覧ができ、Collaborator は閲覧のみ可能です。
したがって、組織 (Organization) には Admin と Collaborator の 2 つのロール (Role) が必要です。
Admin には read:documents
と create:documents
の両方の権限 (Permission) があり、Collaborator には read:documents
権限 (Permission) のみがあります。
- Admin
read:documents
create:documents
- Collaborator
read:documents
ここで Logto の組織テンプレート機能が活躍します。
組織テンプレートは、すべての組織 (Organization) に適用されるアクセス制御モデルの設計図です。どのロール (Role)・権限 (Permission) を持つかを定義します。
なぜ組織テンプレートなのか?
SaaS プロダクトにとってスケーラビリティは最重要要件の 1 つだからです。つまり、1 クライアントで動くものはすべてのクライアントで動く必要があります。
Logto コンソール > 組織テンプレート > 組織権限 (Permission) で read:documents
と create:documents
の 2 つの権限 (Permission) を作成しましょう。
次に組織ロールタブで Admin と Collaborator の 2 つのユーザーロールを作成し、それぞれに対応する権限 (Permission) を割り当てます。
これで各組織 (Organization) の RBAC 権限 (Permission) モデルができました。
次に組織 (Organization) 詳細ページでメンバーに適切なロール (Role) を割り当てます。
これで組織 (Organization) ユーザーにロール (Role) が付きました! これらの操作は Logto Management API でも実現できます:
// 組織 (Organization) 作成者に 'Admin' ロールを割り当て
app.post('/organizations', requireAuth('https://api.documind.com'), async (req, res) => {
const accessToken = await fetchLogtoManagementApiAccessToken();
// Logto で組織 (Organization) 作成
// 既存コード...
// Logto でユーザーを組織 (Organization) に追加
await fetch(`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
userIds: [req.user.id],
}),
});
// 最初のユーザーに `Admin` ロールを割り当て
const rolesResponse = await fetch(`${process.env.LOGTO_ENDPOINT}/api/organization-roles`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
});
const roles = await rolesResponse.json();
// `Admin` ロールを探す
const adminRole = roles.find((role) => role.name === 'Admin');
// 最初のユーザーに `Admin` ロールを割り当て
await fetch(
`${process.env.LOGTO_ENDPOINT}/api/organizations/${createdOrganization.id}/users/${req.user.id}/roles`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
organizationRoleIds: [adminRole.id],
}),
}
);
// 既存コード...
});
これでユーザー権限 (Permission) チェックによる制御が実装できます。
コード上では、ユーザーの組織トークン (Organization token) に権限 (Permission) 情報を持たせ、バックエンドでこれを検証します。
フロントエンドコードの Logto config で、組織 (Organization) 内でユーザーがリクエストする必要のある権限 (Permission) を宣言します。read:documents
と create:documents
を scopes
に追加しましょう。
const config: LogtoConfig = {
endpoint: "<YOUR_LOGTO_ENDPOINT>",
appId: "<YOUR_LOGTO_APP_ID>",
scopes: [UserScope.Organizations, "read:documents", "create:documents"],
resources: [ReservedResource.Organization, "<https://api.documind.com>"], // 新規作成した API リソース識別子
};
いつものように、ユーザーで再ログインして設定を有効にしてください。
次にバックエンドの requireOrganizationAccess
ミドルウェアでユーザー権限 (Permission) の検証を追加します。
const hasRequiredScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};
const requireOrganizationAccess = ({ requiredScopes = [] } = {}) => {
return async (req, res, next) => {
try {
//...
// audience でトークン検証
const { payload } = await jwtVerify(
token,
createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL)),
{
issuer: process.env.LOGTO_ISSUER,
audience: aud,
}
);
//...
// トークンからスコープ取得
const scopes = payload.scope?.split(' ') || [];
// 必要なスコープを検証
if (!hasRequiredScopes(scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}
//...
next();
} catch (error) {
//...
}
};
};
次に POST /documents API を作成し、requireOrganizationAccess
ミドルウェアに requiredScopes 設定を渡してこの API と先ほどの GET /documents
API を保護します。
// ドキュメント作成 API
app.post(
'/documents',
requireOrganizationAccess({ requiredScopes: ['create:documents'] }),
async (req, res) => {
//...
}
);
// ドキュメント取得 API
app.get(
'/documents',
requireOrganizationAccess({ requiredScopes: ['read:documents'] }),
async (req, res) => {
//...
}
);
これでユーザー権限 (Permission) チェックによる制御が実装できました。
フロントエンドでは、組織トークン (Organization token) をデコードするか、Logto の getOrganizationTokenClaims
メソッドでユーザー権限 (Permission) 情報を取得できます。
const [scopes, setScopes] = useState([]);
const { getOrganizationTokenClaims } = useLogto();
const loadScopes = async () => {
const claims = await getOrganizationTokenClaims(organizationId);
setScopes(claims.scope.split(' '));
};
// ...
claims 内のスコープをチェックして、ユーザー権限 (Permission) に応じてページ要素を制御できます。
さらに多くのマルチテナントアプリ機能を追加
ここまでで、マルチテナント SaaS システムの基本的なユーザー・組織 (Organization) 機能を実装できました!ただし、組織 (Organization) ごとのログインページブランディングや、特定ドメインメールの自動組織 (Organization) 追加、エンタープライズレベルの SSO 連携など、まだ触れていない機能もあります。
これらはすべてすぐに使える機能であり、Logto ドキュメントで詳細を確認できます:
- エンタープライズシングルサインオン (SSO) 連携
- ジャストインタイム (JIT) プロビジョニング
- 組織 (Organization) レベルのブランディング
- 組織 (Organization) レベルの多要素認証 (MFA)
- 組織 (Organization) レベルの管理
まとめ
最初は圧倒されそうでしたよね?ユーザー、組織 (Organization)、権限 (Permission)、エンタープライズ機能...果てしない山のように思えました。
でも、ここまでで実現できたことを振り返ってみましょう:
- 複数のサインインオプションと MFA 対応の完全な認証 (Authentication) システム
- 複数所属をサポートする柔軟な組織 (Organization) システム
- 組織 (Organization) 内のロールベースアクセス制御 (RBAC)
そして何より素晴らしいのは、車輪の再発明をせずに済んだことです。Logto のようなモダンなツールを活用することで、数か月かかる開発が数分で完了しました。
本チュートリアルの完全なソースコードは Multi-tenant SaaS Sample で公開しています。
これが 2025 年のモダン開発の力です。インフラに悩まず、独自のプロダクト機能開発に集中できます。さあ、次はあなたの番です。素晴らしいものを作りましょう!
Logto Cloud から Logto OSS まで、Logto のすべての機能を Logto ウェブサイト でチェック、または Logto cloud で今すぐサインアップできます。