🛡️

トークンは通すがテナントは通さない — OAuth 2.0で“許可外API”を終わらせる【Slack/Box実装】

に公開

“テナント越境”を設計で遮断する実務ガイド(図解・実装レシピつき)

はじめに

「社外の Slack ワークスペースにうっかりつないでしまった」「Box の別企業テナントに API を叩いてしまった」――そんな“許可外テナント”問題を実装で防ぐ記事です。やることはシンプル、“合鍵(トークン)を受け取ったら、まず持ち主(テナント)を確認する”。これだけで事故はグッと減ります。

  • クラウドセキュリティの地図(外部攻撃 / 内部不正 / クライアント脆弱性 / APIサーバ脆弱性 / 設定ミス)をサクッと把握
  • 内部不正(Insider Risk)に視点を寄せ、ゼロトラスト的に最小権限・継続検証・監査の考え方を知る
  • OAuth 2.0 で“許可外のテナント/組織”からの API アクセスを止める実装(Slack/Boxのハンズオン)を理解する

💡 なぜ今?
AIプラットフォームの「コネクタ」「プラグイン」「MCP(Model Context Protocol)」「Actions/Tools」など、サービス間連携の仕組みが一気に増えています。多くは裏側で OAuth 2.0 が使われ、エージェントがあなたの代わりに複数のSaaSを横断して操作します。だからこそ、人の誤操作だけでなく自動化フローによる“テナント越境” も起こり得ます。
この流れに合わせて、AI連携にも 行き先固定(Resource Indicators)/送信者拘束(DPoP/mTLS)/即時失効(RFC 7009) を“標準装備”として適用していきましょう。

  • コネクタやMCPの普及で「つながる先」が爆増
  • エージェント実行で権限移譲の頻度が増加(=事故リスクも増える)
  • テナント許可を実装で担保すれば、拡張性と安全性を両立できる

想定読者:
SaaS連携や社内ツールを作っている開発者/SRE/情シス。OAuth 2.0 の単語がぼんやり分かるくらいでOK。


1. まずは地図:クラウドセキュリティの全体像

難しい話の前に“位置関係”を掴んでおくと迷いません。ざっくり次の5レーンで考えます。

  • 外部からの攻撃:脆弱性悪用・盗まれた認証情報・ランサムなど
  • 内部不正(今回の主役):悪意・過失・乗っ取り済み正規アカウントの悪用
  • クライアント/フロントの脆弱性:SPA/モバイルでのトークン漏えい、XSS など
  • API/サーバの脆弱性:認可不備・オブジェクト/プロパティ単位の権限チェック漏れ(→ OWASP API Top 10 が整理)
  • 設定ミス/権限スプロール:広すぎるロール、放置トークン、監査不足

alt text

なぜ“テナント制御”が必要か

OAuth 2.0 は「ユーザが自分のアカウントを使って外部サービスに権限を委譲」できるのが強みですが、マルチテナントの相手先(例:Slackのワークスペース、Boxのエンタープライズ)に対し、どのテナントへの接続を許すかを明確に制御しないと、意図しない組織にデータを渡すリスクが残ります。

参考:


2. 内部不正にズームイン(怖さと設計のコツ)

2-1. 3つの型

  1. 悪意(意図的な持ち出し/破壊)
  2. 過失(誤共有/誤設定)
  3. 乗っ取り(外部攻撃者が正規アカウントのフリ)

2-2. 基本方針(ゼロトラスト3点セット)

  • 最小権限 / JIT付与:必要なときだけ権限を上げ、時間で自動失効
  • 継続的な検証:MFA・デバイス・場所・リスクを都度チェック
  • 監査と検知:API呼び出し・管理操作を高粒度ログで追跡

ここから本題、“どの組織のトークンか?”を毎回確かめる実装に進みます。
実現したいことは以下のようなイメージです。

alt text


3. 設計の背骨:OAuth 2.0 Security BCP(RFC 9700)

“合鍵(トークン)”を安全に配り、別の家(API)で使い回せないようにし、不要になったら即捨てる――これが BCP の考え方です。

※ 以下の機能活用は AS/RS の対応状況に依存 します。Provider が対応していれば有効化を検討(例:Resource Indicators, iss, DPoP など)。


4. 許可テナントチェックの最短ルート

処理の考え方(フロー)

トークンを受け取ったら、まずテナントIDを確定 → 許可リスト照合 → 不一致なら即失効というシンプルな実装を考える。

シーケンス図から見たイメージ


5. 実践①:Slack で“許可ワークスペース/Enterprise”以外を弾く

5-1. 使うもの

5-2. .env(例)

SLACK_TEAM_ID=xxxxxx        # 許可するワークスペースID
SLACK_ENTERPRISE_ID=xxxxx   # 許可するEnterprise ID(Gridの場合)
SLACK_BOT_TOKEN=xoxb-...    # (任意)API呼出や管理用途。署名検証には未使用
SLACK_SIGNING_SECRET=xxxxxx # 署名検証に使用(Webhook/Events)

5-3. Python(最小サンプル)

# Slack: テナントID判定 → 不一致なら即 revoke(Token Rotation対応)
import os
import requests
from typing import Optional

ALLOWED = {v for v in [os.getenv("SLACK_TEAM_ID"), os.getenv("SLACK_ENTERPRISE_ID")] if v}

def on_slack_oauth_exchange_done(access_token: str, refresh_token: Optional[str] = None) -> None:
    """
    OAuth交換直後に呼び出し:
      1) auth.test で team_id / enterprise_id を確認
      2) 許可リスト不一致なら auth.revoke(必要なら refresh も)して例外
      3) 許可なら通常処理へ
    """
    # 1) どの組織のトークンかを確認
    r = requests.post(
        "https://slack.com/api/auth.test",
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=10,
    )
    r.raise_for_status()
    info = r.json()
    if not info.get("ok"):
        # 必要に応じて監査ログなど
        raise RuntimeError(f"auth.test failed: {info}")

    tenant = info.get("enterprise_id") or info.get("team_id")
    allowed = bool(tenant and tenant in ALLOWED)

    if not allowed:
        # 2) 不一致→即失効(access / refresh いずれも対象に)
        #    auth.revoke は単一トークンのみ失効するため、必要に応じて両方実施
        requests.post("https://slack.com/api/auth.revoke", data={"token": access_token}, timeout=10)
        if refresh_token:
            requests.post("https://slack.com/api/auth.revoke", data={"token": refresh_token}, timeout=10)
        # 3) 監査ログや通知はここで
        raise PermissionError("Not in allowlist. Token revoked.")

    # 4) OKなら通常処理へ

補助:署名検証ヘルパ(最初に必ず実行)

# Slack リクエスト署名検証 (X-Slack-Signature / X-Slack-Request-Timestamp)
import os, time, hmac, hashlib

def verify_slack_signature(headers: dict, raw_body: bytes) -> bool:
    signing_secret = os.environ["SLACK_SIGNING_SECRET"]
    # ヘッダ名は大文字/小文字を問わない(case-insensitive)
    timestamp = headers.get("X-Slack-Request-Timestamp") or headers.get("x-slack-request-timestamp")
    slack_sig = headers.get("X-Slack-Signature") or headers.get("x-slack-signature", "")

    # リプレイ対策(例: 5分超は拒否)
    if not timestamp or abs(time.time() - int(timestamp)) > 60 * 5:
        return False

    basestring = f"v0:{timestamp}:{raw_body.decode('utf-8')}"
    my_sig = "v0=" + hmac.new(
        signing_secret.encode("utf-8"),
        basestring.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(my_sig, slack_sig)

5-4.つまずきポイント

  • ワークスペースIDとEnterprise IDのどちらで照合するかを最初に決める(Grid運用なら Enterprise 優先がスッキリ)
  • 署名検証は最初に。遅らせるとDoS偽装の温床に
  • Token Rotation有効時は、auth.revoke は単一トークンのみ失効。完全遮断apps.uninstall / admin.apps.uninstall も検討

6. 実践②:Box で“許可エンタープライズ”以外を弾く

6-1. 使うもの

6-2. .env(例)

BOX_ENTERPRISE_ID=xxxxxx        # 許可するEnterprise ID
BOX_CLIENT_ID=xxxxxx            # revokeに使用
BOX_CLIENT_SECRET=xxxxxx        # revokeに使用

6-3. Python(最小サンプル)

# Box: enterprise.id 照合 → 不一致なら revoke(access / refresh)
import os
import requests

ALLOWED_ENTERPRISE = {os.environ["BOX_ENTERPRISE_ID"]}

def on_box_oauth_exchange_done(access_token: str, refresh_token: str | None = None) -> None:
    """
    OAuth交換直後に呼び出し:
      1) GET /2.0/users/me?fields=enterprise で enterprise.id を取得
      2) 許可外なら /oauth2/revoke (RFC 7009) で access / refresh を即時失効
    """
    me = requests.get(
        "https://api.box.com/2.0/users/me",
        params={"fields": "enterprise"},
        headers={"Authorization": f"Bearer {access_token}"},
        timeout=10,
    )
    me.raise_for_status()
    me = me.json()

    eid = (me or {}).get("enterprise", {}).get("id")
    if not eid or eid not in ALLOWED_ENTERPRISE:
        # revoke (RFC 7009)
        data = {
            "client_id": os.environ["BOX_CLIENT_ID"],
            "client_secret": os.environ["BOX_CLIENT_SECRET"],
            "token": access_token,
        }
        requests.post("https://api.box.com/oauth2/revoke", data=data, timeout=10)
        if refresh_token:
            data["token"] = refresh_token
            requests.post("https://api.box.com/oauth2/revoke", data=data, timeout=10)
        # 監査ログや通知はここで
        raise PermissionError("Not in allowlist. Token revoked.")

    # OKなら通常処理へ

6-4.つまずきポイント

  • enterprise.idユーザ属性から取得する(トークンの発行先を明確化)
  • Refresh Tokenポリシーや失効の運用ルール(期限/再発行)を決めておく(必要に応じて refresh も revoke)

7. 仕上げ:運用で効くチェックリスト


8. 用語ミニ辞典(30秒でおさらい)

  • テナント:SaaSの“組織”単位(Slack はワークスペース/Enterprise、Box は Enterprise)
  • 許可リスト(Allowlist):つないでよいテナント一覧
  • トークンの送信者拘束:トークンを“使える人/クライアント”に結び付ける(盗まれても他人が使えない)
  • ミックスアップ攻撃:違う発行者の応答を混ぜて混乱させる手口(→ iss で緩和)

9. 余談:ネットワーク側でブロックする選択肢

Microsoft 環境なら Tenant Restrictions v2 で許可テナント以外の認証をネットワークで遮断できます。
ネットワーク側の広域遮断(TRv2)とアプリ側の細粒度検証(本記事)の役割分担を意識できると盤石です。要件にハマるなら強力。
https://learn.microsoft.com/entra/external-id/tenant-restrictions-v2


参考リンク(公式中心)

Discussion