クラウドSDKを用いたマルチテナント認証・認可基盤(RBAC)の実装設計パターンとセキュリティ考慮点
はじめに
こんにちは、NKKTech Global 技術チームです。
B2B SaaSの開発において、避けて通れないのが「マルチテナント管理」と「きめ細やかな権限管理(RBAC)」です。
- 「テナントAのユーザーが、URLのIDを書き換えてテナントBのデータを見れてしまった(BOLA / IDOR脆弱性)」
- 「管理者(Admin)と一般メンバー(Member)の認可ロジックが各APIに散乱し、権限チェックの抜け漏れが発生した」
これらは、認証(誰であるか)と認可(何をしてよいか)がアーキテクチャレベルで分離・統一されていないことに起因します。本記事では、クラウドSDK(AWS Cognito / STS / GCP Identity Platform等)を軸にした、堅牢なマルチテナントRBACの実装設計パターンを解説します。
1. テナント分離の3大アーキテクチャパターン
マルチテナントの認証基盤は、コストとセキュリティ要件(コンプライアンス)のトレードオフで3つに分類されます。
[ 1. プール型 (Pooled) ] ── 中小規模SaaS / コスト最小化
1つのユーザープール / 1つのDB (Row-Level Securityで論理分離)
トークン内の "tenant_id" クレームで境界を担保
[ 2. サイロ型 (Siloed) ] ── エンタープライズ・金融向け / 完全隔離
テナントごとに専用のユーザープール & 専用DBをプロビジョニング
物理的にインフラが分離されているため漏洩リスク最小
[ 3. ハイブリッド型 (Hybrid) ] ── 実務で最も選ばれる構成
認証プールは全テナント共有 + DBはスキーマ分離または行レベルセキュリティ
特定の大手顧客のみサイロ型へ切り出す柔軟性を持つ
2. JWTクレーム設計とコンテキスト検証の鉄則
認可エラーやなりすましを防ぐため、JWT(JSON Web Token)に含めるクレーム設計が極めて重要です。
推奨されるアクセストークンのペイロード設計
{
"sub": "usr_99a8b7c6-d5e4-4f3a",
"iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_XXXXX",
"exp": 1776300000,
"custom:tenant_id": "tenant_enterprise_01",
"custom:roles": ["TenantAdmin"],
"custom:permissions": [
"users:read",
"users:write",
"billing:read"
]
}
【鉄則】リクエストボディやクエリの tenant_id を信用しない
APIエンドポイント(例: GET /api/v1/orders?tenant_id=XXX)でクライアントから渡されるテナントIDをそのまま信用してDBクエリを発行してはいけません。
必ず署名検証済みのJWTペイロードから取り出した tenant_id をバックエンドの実行コンテキストにセットしてください。
3. 実装パターン:クラウドSDKとミドルウェアによる認可制御
以下は、Node.js / Express環境において、AWS SDK(Cognito JWT検証)と連携し、マルチテナントRBACを自動適用するミドルウェアの実装例です。
① 認証・テナントコンテキスト注入ミドルウェア (authMiddleware.ts)
import { Request, Response, NextFunction } from 'express';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
// AWS Cognito JWT Verifierの初期化
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID!,
tokenUse: 'access',
clientId: process.env.COGNITO_CLIENT_ID!,
});
export interface TenantContext {
userId: string;
tenantId: string;
roles: string[];
permissions: string[];
}
declare global {
namespace Express {
interface Request {
tenantContext?: TenantContext;
}
}
}
export const authenticateTenant = async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or malformed token' });
}
const token = authHeader.split(' ')[1];
// 1. JWTの暗号署名・有効期限の検証
const payload = await verifier.verify(token);
// 2. トークンからテナントIDと権限を安全に抽出
const tenantId = payload['custom:tenant_id'] as string;
const roles = (payload['custom:roles'] as string[]) || [];
const permissions = (payload['custom:permissions'] as string[]) || [];
if (!tenantId) {
return res.status(403).json({ error: 'Tenant context missing in token' });
}
// 3. リクエストスコープにテナントコンテキストをバインド
req.tenantContext = {
userId: payload.sub,
tenantId,
roles,
permissions
};
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token', details: err });
}
};
② RBAC認可ガードミドルウェア (requirePermission.ts)
import { Request, Response, NextFunction } from 'express';
export const requirePermission = (requiredPermission: string) => {
return (req: Request, res: Response, next: NextFunction) => {
const context = req.tenantContext;
if (!context) {
return res.status(401).json({ error: 'Unauthenticated' });
}
// 管理者ロールは全権限バイパス、または特定パーミッションの保有確認
const hasRole = context.roles.includes('SuperAdmin') || context.roles.includes('TenantAdmin');
const hasPermission = context.permissions.includes(requiredPermission);
if (!hasRole && !hasPermission) {
return res.status(403).json({
error: 'Forbidden: Insufficient permissions for this tenant resource'
});
}
next();
};
};
// APIルート定義での適用例
// app.delete('/api/v1/users/:id', authenticateTenant, requirePermission('users:delete'), deleteUserHandler);
③ PostgreSQL 行レベルセキュリティ(RLS)との連携
ミドルウェアで取得した tenantId をデータベースセッションに設定することで、万が一SQLのWHERE句にテナント指定を書き忘れても、DBエンジンが物理的に他社データの取得を遮断します。
// トランザクション開始時にセッション変数をセット
await db.query('SET LOCAL app.current_tenant_id = $1', [req.tenantContext.tenantId]);
// PostgreSQL側のポリシー定義:
// CREATE POLICY tenant_isolation_policy ON orders
// USING (tenant_id = current_setting('app.current_tenant_id'));
4. マルチテナントSaaSで必須のセキュリティ考慮点
① BOLA / IDOR (Broken Object Level Authorization) の完全防止
攻撃者が GET /api/documents/1005 をリクエストした際、ドキュメント1005が「リクエスト元のテナントに属しているか」を必ず検証しなければなりません。
-
対策: DBクエリで
SELECT * FROM documents WHERE id = 1005 AND tenant_id = :tenantIdを強制する、または前述のPostgreSQL RLSを全テーブルに適用する。
② Redis・Memcached等のキャッシュ汚染(Cache Bleed)の防止
分散キャッシュを使用する場合、キー名にテナントIDを含めないと他テナントにキャッシュが漏洩します。
-
危険:
cache.get("user:123") -
安全:
cache.get("tenant:" + tenantId + ":user:123")
③ テナント単位の監査ログ(Audit Trail)の分離
誰が・いつ・どのテナントで権限を行使したかを、改ざん不可能な形式で記録します。
- AWS CloudWatch LogsやDatadogにログを送る際、必ず
tenant_idを構造化JSONの最上位フィールドに配置し、テナント別のインシデント調査を即座に行えるように設計します。
まとめ:マルチテナントセキュリティは「多層防衛」で完結する
マルチテナント基盤のセキュリティは、アプリケーションコード単体で担保しようとしてはいけません。
- クラウドIdP(Cognito等): JWTクレームによる改ざん不能なテナント情報の署名
- APIゲートウェイ・ミドルウェア: 一元化されたRBACチェックとコンテキスト注入
- データベース(RLS / スキーマ分離): アプリ側のバグを許容しないDB層での物理・論理遮断
この多層防衛アーキテクチャを敷くことで、将来テナント数や開発メンバーが急増しても、安心・安全にスケールできるエンタープライズ品質のSaaS基盤が構築できます。
お問い合わせ先
マルチテナントSaaSの認証・認可設計、クラウドインフラ構築、エンタープライズ向けセキュリティ監査に関するご相談は、下記よりお気軽にお問い合わせください。
- Webサイト: https://nkktech.com/
- メール: contact@nkk.com.vn
- LinkedIn: https://www.linkedin.com/company/nkktech
著者:NKKTech Global 技術チーム
私たちは、強固なセキュリティ設計とモダンなクラウドアーキテクチャで、グローバルに戦えるSaaSプロダクトの開発を支援します。
