トークンは通すがテナントは通さない — 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 が整理)
- 設定ミス/権限スプロール:広すぎるロール、放置トークン、監査不足
なぜ“テナント制御”が必要か
OAuth 2.0 は「ユーザが自分のアカウントを使って外部サービスに権限を委譲」できるのが強みですが、マルチテナントの相手先(例:Slackのワークスペース、Boxのエンタープライズ)に対し、どのテナントへの接続を許すかを明確に制御しないと、意図しない組織にデータを渡すリスクが残ります。
参考:
- OWASP API Top 10(2023):https://owasp.org/API-Security/editions/2023/en/0x00-header/
- MITRE ATT&CK for Cloud:https://attack.mitre.org/matrices/enterprise/cloud/
- NIST SP 800-207(Zero Trust):https://csrc.nist.gov/publications/detail/sp/800-207/final
- Verizon DBIR 2024:https://www.verizon.com/business/resources/reports/2024-data-breach-investigations-report.pdf
2. 内部不正にズームイン(怖さと設計のコツ)
2-1. 3つの型
- 悪意(意図的な持ち出し/破壊)
- 過失(誤共有/誤設定)
- 乗っ取り(外部攻撃者が正規アカウントのフリ)
2-2. 基本方針(ゼロトラスト3点セット)
- 最小権限 / JIT付与:必要なときだけ権限を上げ、時間で自動失効
- 継続的な検証:MFA・デバイス・場所・リスクを都度チェック
- 監査と検知:API呼び出し・管理操作を高粒度ログで追跡
ここから本題、“どの組織のトークンか?”を毎回確かめる実装に進みます。
実現したいことは以下のようなイメージです。
3. 設計の背骨:OAuth 2.0 Security BCP(RFC 9700)
“合鍵(トークン)”を安全に配り、別の家(API)で使い回せないようにし、不要になったら即捨てる――これが BCP の考え方です。
- Authorization Code + PKCE を既定(Implicit は使わない)
- PAR(RFC 9126)/ JAR(RFC 9101) で認可リクエスト改ざんを抑止
-
Issuer Identification
iss
(RFC 9207) でミックスアップ攻撃を緩和 - Resource Indicators(RFC 8707) で**トークンの行き先(Audience)**を固定
- 短命+回転+即時失効(RFC 7009) を運用に組み込む
- 送信者拘束:DPoP(RFC 9449) or mTLS(RFC 8705)
- BCP 本体:RFC 9700 https://www.rfc-editor.org/rfc/rfc9700
※ 以下の機能活用は AS/RS の対応状況に依存 します。Provider が対応していれば有効化を検討(例:Resource Indicators,
iss
, DPoP など)。
4. 許可テナントチェックの最短ルート
処理の考え方(フロー)
トークンを受け取ったら、まずテナントIDを確定 → 許可リスト照合 → 不一致なら即失効というシンプルな実装を考える。
シーケンス図から見たイメージ
5. 実践①:Slack で“許可ワークスペース/Enterprise”以外を弾く
5-1. 使うもの
-
だれのトークン? →
auth.test
(team_id
/enterprise_id
) -
不一致なら即失効 →
auth.revoke
、アプリごと外す →apps.uninstall
(Enterprise はadmin.apps.uninstall
)- revoke: https://docs.slack.dev/reference/methods/auth.revoke/
- apps.uninstall: https://api.slack.com/methods/apps.uninstall
- admin.apps.uninstall: https://api.slack.com/methods/admin.apps.uninstall
-
Token Rotation有効時:
auth.revoke
は単一トークンのみを失効(access か refresh)。完全遮断したい場合はapps.uninstall
を推奨。
-
受信署名の検証(必須) →
X-Slack-Signature
/X-Slack-Request-Timestamp
-
(運用)トークン回転
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. 使うもの
-
だれのトークン? →
GET /2.0/users/me?fields=enterprise
(enterprise.id
) -
不一致なら即失効 →
POST /oauth2/revoke
(RFC 7009 準拠) -
Webhook 署名検証 →
BOX-SIGNATURE-PRIMARY/SECONDARY
(HMAC-SHA256)
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. 仕上げ:運用で効くチェックリスト
-
ゼロトラスト:最小権限 / 必要時だけ昇格(JIT) / 継続検証(デバイス・場所・リスク)
- NIST SP 800-207:https://csrc.nist.gov/publications/detail/sp/800-207/final
-
Code + PKCE を既定(Implicit は使わない)
-
PAR / JAR /
iss
でリクエスト改ざん・ミックスアップ緩和 -
Resource Indicators でトークンの行き先固定(別API流用を抑止)
-
短命+回転+即時失効(RFC 7009)を運用に
-
DPoP or mTLS で送信者拘束(盗難トークンの再利用を困難化)
-
Slack:
auth.test
でteam_id / enterprise_id
判定 → 不一致はauth.revoke
/apps.uninstall
(Enterprise はadmin.apps.uninstall
)- auth.test:https://api.slack.com/methods/auth.test / revoke:https://docs.slack.dev/reference/methods/auth.revoke/
- apps.uninstall:https://api.slack.com/methods/apps.uninstall / admin 版:https://api.slack.com/methods/admin.apps.uninstall
-
Box:
users/me?fields=enterprise
でenterprise.id
判定 → 不一致は/oauth2/revoke
-
署名検証(Slack/BoxのHMAC)を最初に実施
-
トークン保存は平文禁止:DBには KMS等で暗号化保存+参照用ハッシュを別途持つ
-
監査ログの粒度:tenant_id / provider / user_id / token_hash / reason / ip / ua / req_id / decided_by を記録
-
失効APIのフェイルセーフ:Revoke/Uninstall が失敗してもブロックは継続。リトライ/DLQ を設計
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
参考リンク(公式中心)
-
OAuth 2.0 Security BCP(RFC 9700):https://www.rfc-editor.org/rfc/rfc9700
-
PKCE(RFC 7636):https://www.rfc-editor.org/rfc/rfc7636
-
PAR(RFC 9126):https://www.rfc-editor.org/rfc/rfc9126 / JAR(RFC 9101):https://www.rfc-editor.org/rfc/rfc9101
-
Issuer Identification(RFC 9207):https://www.rfc-editor.org/rfc/rfc9207
-
Resource Indicators(RFC 8707):https://www.rfc-editor.org/rfc/rfc8707
-
Token Revocation(RFC 7009):https://www.rfc-editor.org/rfc/rfc7009
-
DPoP(RFC 9449):https://www.rfc-editor.org/rfc/rfc9449 / mTLS(RFC 8705):https://www.rfc-editor.org/rfc/rfc8705
-
NIST SP 800-207(Zero Trust):https://csrc.nist.gov/publications/detail/sp/800-207/final
-
OWASP API Security Top 10(2023):https://owasp.org/API-Security/editions/2023/en/0x00-header/
-
MITRE ATT&CK for Cloud:https://attack.mitre.org/matrices/enterprise/cloud/
-
Verizon DBIR 2024:https://www.verizon.com/business/resources/reports/2024-data-breach-investigations-report.pdf
-
Slack Docs:
- auth.test:https://api.slack.com/methods/auth.test
- auth.revoke:https://docs.slack.dev/reference/methods/auth.revoke/
- apps.uninstall:https://api.slack.com/methods/apps.uninstall
- admin.apps.uninstall:https://api.slack.com/methods/admin.apps.uninstall
- 署名検証:https://docs.slack.dev/authentication/verifying-requests-from-slack
- トークン回転:https://api.slack.com/authentication/rotation
-
Box Docs:
Discussion