← MCPで Claude を業務システムと統合する
LESSON 04 / 06

認証・権限管理:本番想定の設計

所要時間 14分 上級レベル

本レッスンでは、MCP サーバーを マルチユーザー本番環境 で運用するための認証・認可を扱います。

MCP サーバーの典型的な認証ニーズ

レベル 必要な認証
1. 単一ユーザー 個人開発・PoC 不要
2. 単一組織内 社内ツール SSO(社内認証統合)
3. SaaSとして提供 有料プロダクト OAuth2 + JWT
4. マルチテナント 大規模B2B テナント分離 + RBAC

認証フローの設計

# MCP サーバー起動時に資格情報を渡すパターン

# Claude Desktop config:
{
  "mcpServers": {
    "myapp": {
      "command": "python",
      "args": ["server.py"],
      "env": {
        "API_KEY": "sk-...",
        "USER_ID": "user_12345"
      }
    }
  }
}

# サーバー側で取得
import os
API_KEY = os.environ["API_KEY"]
USER_ID = os.environ["USER_ID"]

OAuth フローの実装

MCP サーバーが SaaS(Slack/Notion/Google)を呼ぶ場合、ユーザーごとの OAuth トークンが必要です。

# oauth_mcp.py
from mcp.server.fastmcp import FastMCP
import requests

mcp = FastMCP("oauth-mcp")

# 起動時に必要な情報
USER_ID = os.environ["USER_ID"]
TOKEN_STORE_URL = os.environ["TOKEN_STORE_URL"]


def get_user_token(user_id: str, provider: str) -> str:
    """セキュアなトークン保管庫から取得。"""
    response = requests.get(
        f"{TOKEN_STORE_URL}/tokens",
        params={"user_id": user_id, "provider": provider},
        headers={"X-Service-Auth": os.environ["SERVICE_AUTH_KEY"]},
    )
    response.raise_for_status()
    return response.json()["access_token"]


@mcp.tool()
def post_to_slack(channel: str, message: str) -> str:
    """ユーザーのSlackに投稿(OAuth認証)。"""
    token = get_user_token(USER_ID, "slack")
    response = requests.post(
        "https://slack.com/api/chat.postMessage",
        headers={"Authorization": f"Bearer {token}"},
        json={"channel": channel, "text": message},
    )
    if response.json().get("ok"):
        return "投稿完了"
    return f"エラー: {response.json().get('error')}"

権限スコープの管理

# スコープ定義
SCOPES = {
    "read_only": ["search", "list", "get"],
    "write": ["create", "update"],
    "admin": ["delete", "manage_users"],
}


def require_scope(scope: str):
    """ツールに必要なスコープをデコレータで強制。"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            user_scopes = get_user_scopes(USER_ID)
            if scope not in user_scopes:
                return f"権限不足: {scope} が必要です"
            return func(*args, **kwargs)
        return wrapper
    return decorator


@mcp.tool()
@require_scope("write")
def create_record(data: dict) -> str:
    """レコード作成(write 権限が必要)。"""
    ...


@mcp.tool()
@require_scope("admin")
def delete_user(user_id: int) -> str:
    """ユーザー削除(admin 権限が必要)。"""
    ...

マルチテナント分離

# テナント分離の実装

@mcp.tool()
def search_records(query: str) -> str:
    """レコード検索(テナント内のみ)。"""
    tenant_id = get_user_tenant(USER_ID)

    # 必ずテナントIDで絞り込み
    sql = """
        SELECT * FROM records
        WHERE tenant_id = %s AND query MATCH %s
        LIMIT 100
    """
    return execute_query(sql, [tenant_id, query])


# DB レベルでも RLS(Row Level Security)を設定
"""
ALTER TABLE records ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON records
    USING (tenant_id = current_setting('app.current_tenant_id')::int);

# クエリ前にテナントを設定
SET app.current_tenant_id = '12345';
"""

PII(個人情報)保護

# PII を含む結果を自動マスキング

import re

def mask_pii(text: str) -> str:
    # メールアドレス
    text = re.sub(r'b[w.-]+@[w.-]+.w+b', '[EMAIL]', text)
    # 電話番号(日本)
    text = re.sub(r'b0d{1,4}-d{1,4}-d{3,4}b', '[PHONE]', text)
    # クレジットカード
    text = re.sub(r'bd{4}[-s]?d{4}[-s]?d{4}[-s]?d{4}b', '[CC]', text)
    # マイナンバー(12桁)
    text = re.sub(r'bd{12}b', '[MY_NUMBER]', text)
    return text


@mcp.tool()
def search_customer_logs(query: str) -> str:
    raw_result = _do_search(query)
    return mask_pii(raw_result)

監査ログの実装

# 全ツール呼び出しを監査ログに記録

def audit_log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        log_entry = {
            "timestamp": datetime.now().isoformat(),
            "user_id": USER_ID,
            "tool": func.__name__,
            "args": redact_sensitive(kwargs),
        }
        try:
            result = func(*args, **kwargs)
            log_entry["status"] = "success"
            log_entry["result_size"] = len(str(result))
        except Exception as e:
            log_entry["status"] = "error"
            log_entry["error"] = str(e)
            raise
        finally:
            audit_db.insert(log_entry)

        return result
    return wrapper


@mcp.tool()
@audit_log
def sensitive_operation(args):
    ...

セキュリティチェックリスト

項目 確認
認証必須 すべてのツールがユーザー識別済み
認可スコープ ツールごとに必要権限を定義
テナント分離 クエリにtenant_id を必ず含める
SQL インジェクション パラメータ化クエリ徹底
PII マスキング 結果に自動マスキング
レート制限 ユーザー単位・ツール単位
シークレット管理 環境変数または Vault 経由
監査ログ 全呼び出しを記録
HTTPS HTTPトランスポートの場合は TLS必須
依存パッケージ 定期的な脆弱性スキャン

失敗パターン

失敗 対処
テナントID漏れで他組織データ閲覧 RLS とアプリ層の二重チェック
ログにシークレットを記録 redact_sensitive() で自動マスキング
OAuth トークンのリフレッシュ忘れ トークン取得時に有効期限管理
過剰権限のサービスアカウント 最小権限原則、ロール定期見直し

このレッスンのまとめ

本番運用のMCPサーバーは「OAuth → スコープ → テナント分離 → PII保護 → 監査」が必須。次のレッスンでは、Claude Desktop / API クライアント側の実装を学びます。

よくある質問

この記事に関連する質問と答えをまとめました。

Q.マルチテナント環境での認証実装は?
A.
OAuth → JWT スコープ → tenant_id 分離 → DB レベルRLS(Row Level Security)の4層構造が定石です。アプリ層と DB 層の二重チェックで漏洩を防ぎます。
Q.PII(個人情報)の自動マスキングはどう実装?
A.
メール・電話・カード番号・マイナンバーを正規表現で検出し、トークン置換するのが基本です。MCP サーバーの全結果に共通ミドルウェアとして適用するのが安全です。