Azure Functions と Bot Framework(Python)で作ったボットが「受信はできるのに、返信時だけ 401/Unauthorized」となる――この現象は設定と認証の“噛み合わせ”の問題で起きます。本稿では実際の障害ログを手掛かりに、原因の切り分けから恒久対応までを体系化。Graph 権限の誤解、テナント種別(Single/Multi)、資格情報の充て方、リージョン整合、そして Azure Functions 特有の落とし穴まで、現場で役立つ具体策を一気通貫でまとめます。
ボット返信時に発生する “Operation returned an invalid status code ‘Unauthorized’” エラー
質問の背景
Azure Functions + Bot Framework(Python)のボットで、以下の症状が出るケースがあります。
- 受信メッセージは 常に成功(Webhook とエンドポイントは動作)
- しかし Web Chat や Slack へ 返信 を送ると 401/Unauthorized(Bot Framework コネクタ側で拒否)
- App 登録をマルチテナントにし、
Chat.ReadWrite/ChatMessage.Send/ChannelMessage.Send等を付けても改善しない
典型的な診断ログ例
Bot adapter credentials not found
Region configured (None) != canadacentral
Error sending error message: … ‘Unauthorized’
結論(要点サマリ)
本件の本質は「ボットがどのアイデンティティで、どのエンドポイント(Audience)に対して、どのトークンを提示しているか」に尽きます。以下の表で、現場で最も多い原因と対策を先に押さえておきましょう。
| 発生原因 | 対応策 |
|---|---|
| 権限の種類が不適切 Graph のチャット/メッセージ系権限を増やしても、Bot Framework での返信には効かない。多くがユーザー委任(Delegated)前提で、ボット(アプリ)には無関係。 | Graph 権限を外し、Bot Channel Registration が持つ “Bot Service” の Application 資格情報のみで送信する。必要に応じて Managed Identity を使って資格情報の安全な供給に切り替える。 |
| シングル/マルチテナント不整合 Bot リソースは Single tenant 前提で動かすのが堅実。Multi-tenant のままだと、認証時の Authority/Aud の不一致により 401 が起きやすい。 | App Registration を Single tenant に固定し、トークン取得時の authority を https://login.microsoftonline.com/{TenantId} に明示する。 |
クライアント資格情報が空Bot adapter credentials not found が出ている。 | Functions/App Service のアプリ設定に MicrosoftAppId と MicrosoftAppPassword(ClientSecret) を正しく格納。必要に応じて MicrosoftAppType と MicrosoftAppTenantId も明示。 |
リージョン不一致Region configured (None) != canadacentral など。 | Bot リソースのリージョンと Messaging Endpoint、そして(必要に応じて)SDK/設定側のリージョン指定を一致させる。グローバル環境/政府クラウド等の混在にも注意。 |
最終的に解決した手順(実録)
- App Registration を Single tenant に統一
- 機密クライアントで
https://login.microsoftonline.com/{APP_TENANT_ID}に対してトークンを取得- 取得したトークンで Bot Framework へ送信(SDK に委任 or MSAL 手動)
これにより返信が正常に送信できるようになり、“Unauthorized” は消失。
なぜ「受信できる」のに「返信で 401」なのか
Bot Framework では、受信(ユーザー→ボット)はチャンネル側(Web Chat/Teams/Slack 等)から Webhook に対して POST が飛んできます。この呼び出しは Bot Service→あなたのエンドポイントという サーバー間 通信で、あなたが AAD トークンを提示する場面はありません。
一方、返信(ボット→ユーザー)は、あなた(ボット) が Bot Service のコネクタ API へアクセスして送信します。このとき必要なのは ボットのアプリケーション資格情報 で発行したトークンです。ここで AppId/Secret/Tenant が欠けていたり、Authority がズレていたりすると 401 になります。つまり、受信と返信で認証の向きが違うのがポイントです。
正しい認証モデル:Single tenant + アプリケーション資格情報
Azure ポータル側の基本構成
- Bot Channels Registration(または Web App Bot/Azure Bot)を作成
- 紐づく App Registration は Single tenant に設定
- クライアント シークレット(Client Secret)を発行し、安全に保管(Key Vault 推奨)
- Bot の Messaging endpoint を
https://<your-func>.azurewebsites.net/api/messages(例)に設定 - 接続したいチャネル(Web Chat/Teams/Slack)を Bot チャネルで有効化
Azure Functions(App Settings)に入れる環境変数
| キー | 例 | 意味/補足 |
|---|---|---|
| MicrosoftAppId | 00000000-0000-0000-0000-000000000000 | ボットのアプリケーション(クライアント)ID |
| MicrosoftAppPassword | (長いシークレット文字列) | ボットのクライアント シークレット(Client Secret) |
| MicrosoftAppType | SingleTenant | 認証タイプ。SingleTenant を明示すると検証が安定 |
| MicrosoftAppTenantId | 11111111-1111-1111-1111-111111111111 | 発行元テナントの ID(Authority の固定) |
| WEBSITE_RUN_FROM_PACKAGE | 1 | Functions のデプロイ安定化(任意) |
| FUNCTIONS_WORKER_PROCESS_COUNT | 1〜2 | 同時処理調整(スロットリング時の安定性向上) |
| BOT_SERVICE_REGION | canadacentral など | リージョン明示が必要な環境で使用(後述) |
Python(Bot Framework v4:CloudAdapter)の最小実装例
SDK に資格情報の取得とトークン管理を任せるのが最も堅牢です。以下は Azure Functions での最小構成の一例です(概念サンプル)。
# requirements.txt(抜粋)
# botbuilder-core==4.x
# botbuilder-schema==4.x
# botbuilder-integration-aiohttp==4.x など
import os
import json
import azure.functions as func
from botbuilder.core import (
CloudAdapter,
TurnContext,
MessageFactory,
ConfigurationServiceClientCredentialFactory,
)
from botbuilder.schema import Activity
# 環境変数から資格情報を読むファクトリ
credentials_factory = ConfigurationServiceClientCredentialFactory()
# CloudAdapter に資格情報を注入
adapter = CloudAdapter(credentials_factory)
# ボット本体(極小)
async def on_turn(turn_context: TurnContext):
if turn_context.activity.type == "message":
await turn_context.send_activity(MessageFactory.text("こんにちは!返信できています。"))
# Azure Functions エントリポイント
async def main(req: func.HttpRequest) -> func.HttpResponse:
body = req.get_body()
activity = Activity().deserialize(json.loads(body))
auth_header = req.headers.get("Authorization", "")
# CloudAdapter がトークンを処理し、Bot Service へ正しく返信する
response = await adapter.process_activity(activity, auth_header, on_turn)
return func.HttpResponse(status_code=200)
ポイント:
ConfigurationServiceClientCredentialFactoryがMicrosoftAppId/MicrosoftAppPassword/MicrosoftAppType/MicrosoftAppTenantIdを読み取り、必要なトークン(Audience: Bot Framework)を取得します。- あなたが Graph のスコープ を自前で組み立てる必要はありません(Bot 経由の送信には不要)。
MSAL を使う「手動トークン」方式(必要な場合のみ)
特殊要件でトークン取得を自前で制御したい場合は、MSAL の機密クライアント(Confidential Client)で Bot Service をリソースにしたトークンを取り、SDK に注入します。概念コード:
# pip install msal
import os
import msal
TENANT = os.getenv("MicrosoftAppTenantId")
CLIENT_ID = os.getenv("MicrosoftAppId")
CLIENT_SECRET = os.getenv("MicrosoftAppPassword")
app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=f"[https://login.microsoftonline.com/{TENANT}](https://login.microsoftonline.com/{TENANT})",
)
# Bot Framework の既定リソース(Audience)
SCOPE = ["[https://api.botframework.com/.default](https://api.botframework.com/.default)"]
result = app.acquire_token_for_client(scopes=SCOPE)
access_token = result["access_token"]
# 以降、Connector 呼び出しに Authorization: Bearer {access_token} を付与
# 実務では SDK(CloudAdapter)に委任する方が安全・簡潔です。
注意:これは Graph 用のトークンではありません。Audience が Bot Service になっていることが重要です。
リージョン不一致エラーの対処
Region configured (None) != canadacentral のようなログは、Bot リソース/Messaging Endpoint/SDK 設定のいずれかで リージョンの台形不一致 があるときに出やすい症状です。次を確認します。
- Bot リソース作成時のリージョン(例:Canada Central)
- Functions(App Service)のホストリージョン(できる限り同一に)
- Messaging Endpoint が該当リージョンのインフラで応答できているか
- 政府クラウド/ドメイン分離(GCC/GCC High)などの Channel Service を使っていないか
Public(商用)クラウドと政府クラウドで トークンの検証先 や サービス URL が異なるため、CloudAdapter/Connector の設定(例:Channel Service)を合わせ込みます。単に「None」になっている場合は、環境変数の読み込み漏れも疑いましょう。
「Graph 権限を増やしたのに直らない」理由
Teams や Slack など 各チャネルへの投稿 は、Bot Framework の コネクタ が代理送信します。あなたが Graph API に直接 POST するわけではありません(Teams へ Graph でメッセージ投稿する用途は別設計)。よって、ボット返信の 401 は Graph のスコープ追加では直りません。むしろ権限を増やすほど運用負債になります。
- Graph によるメッセージ送信は 別の統合方式(アプリケーション権限/委任権限を含む)であり、Bot Framework の返信チャネルとは無関係。
- ボット返信は「Bot Service の Audience へ正しいトークンを出す」ことだけに集中する。
- 将来的に Graph も使うなら、別経路 として設計・実装を分離し、最小権限で付与。
Azure Functions 特有の落とし穴
1) アプリ設定の誤り・空値
Bot adapter credentials not found が出る場合、MicrosoftAppId/MicrosoftAppPassword/MicrosoftAppType/MicrosoftAppTenantId のいずれかが未設定・スペルミス・スロット設定漏れである可能性が高いです。デプロイスロット使用時は スロット固有設定 の引き継ぎにも注意。
2) 関数の認証レベル
Function の HTTP トリガーが Function レベルのままでも、Bot Service からは正しいキーが付与されて呼ばれます。もし Anonymous に変えて一旦改善するなら、プロキシ/WAF/API Management などの手前でヘッダーが落ちている可能性を疑います。
3) タイムアウトとウォームアップ
コールドスタート時に最初の返信がこけると「Unauthorized」と誤認することがあります。常時起動(Always On)、適正な プラン、起動時ウォームアップ(Durable/Timer)を検討しましょう。
4) ミドルウェア順番・例外処理
例外時に エラーメッセージ送信 を別スレッドで発火させると、認証準備前に送信して 401 になることがあります。CloudAdapter の標準エラー ハンドラを用い、await を忘れないこと。
Slack/Web Chat/Teams チャネル別の注意点
Slack
- Bot Framework 側で Slack チャネルを有効化し、Slack の認可は チャネル接続 に紐づく。
- あなたのコードから Slack API へ直接投げる必要はない。401 は Bot Service 側の認証が主因。
Web Chat
- クライアント側の Direct Line トークン生成は 別物。サーバー側の返信 401 とは切り分けて考える。
Microsoft Teams
- Graph のチャット投稿と Bot の投稿は経路が違う。ボット返信に Graph 権限は不要。
- テナント境界を跨ぐ場合は、アプリの組織許可とボットのチーム/個人へのインストールを正しく実施。
切り分けのための即効チェックリスト
- App Registration は Single tenant か(Multi のままにしていないか)
MicrosoftAppIdとMicrosoftAppPasswordは正しいか(コピペ/改行混入ミスなし)MicrosoftAppType=SingleTenant、MicrosoftAppTenantIdが入っているか- Messaging Endpoint は
/api/messagesに向いているか - Functions の 構成スロット にも同じ環境変数が入っているか
- リージョンは Bot/Functions/ストレージで一致しているか
- CloudAdapter を使っているか(旧 Adapter の混在がないか)
- 独自に MSAL で Graph のトークンを取って 使い回していない か
- 政府クラウドと商用クラウドの設定を混在させていないか
- 例外ハンドラから送る エラーメッセージ が 401 を誘発していないか
Managed Identity(UAMI)を使った運用のコツ
将来的に複数テナントへ展開する、あるいはシークレット管理を省力化したい場合は User‑Assigned Managed Identity(UAMI) を検討します。Key Vault から App Secret を配布する構成でも良いですが、UAMI によって「誰がどのリソースにアクセスできるか」を Azure RBAC で集中的に管理できます。
- Functions に UAMI を割り当て、必要な構成情報(AppId/TenantId 等)を Key Vault に格納
- 起動時に UAMI で Key Vault から値を取得し、環境変数に流し込む(またはコード直接利用)
- ローテーションや権限剥奪が「ID 側」で完結。シークレットの配布・再デプロイを最小化
なお、UAMI は「Bot Service への送信トークン」を直接発行するわけではありません。資格情報の安全な調達手段として活用し、最終的な Audience は Bot Service へ向けます。
ログの読み方と対処の当たり所
| ログ断片 | 意味 | 具体的対処 |
|---|---|---|
Bot adapter credentials not found | Adapter が AppId/Secret を取れていない | App Settings のキー名と値、スロットの継承、アクセス権(Key Vault 参照権)を再確認 |
Unauthorized(返信時) | Audience(Bot Service)に対する Bearer が不正/欠落 | SingleTenant で Authority 固定、CloudAdapter 使用、Bot Service Audience のトークンを生成 |
Region configured (None) != {region} | リージョン設定の不一致/未設定 | Bot/Functions/チャネル設定を同一リージョンに、必要なら BOT_SERVICE_REGION 等で明示 |
Error sending error message: ... | 例外時の「エラー通知」自体が 401 | ハンドラ順序と await を見直し、まず認証基盤(Adapter)を初期化してから送信 |
最短で直すための実行プラン
- App Registration を Single tenant に統一(テナント ID を控える)
- Functions のアプリ設定に
MicrosoftAppId/MicrosoftAppPassword/MicrosoftAppType=SingleTenant/MicrosoftAppTenantIdを投入 - CloudAdapter +
ConfigurationServiceClientCredentialFactoryに切り替え(旧 Adapter なら移行) - Bot リソースの Messaging Endpoint を再保存(疎通テスト)
- リージョンの整合(Bot/Functions/ストレージ/チャネル)を確認
- 本番前に Web Chat と Teams の双方向疎通を手元で検証(Slack を使う場合はチャンネル接続の認可も再確認)
補足:最小ボットの完全サンプル(概念・Functions 版)
実装の雰囲気を掴むための概念サンプルです。プロダクションではログ/リトライ/タイムアウト/ストレージ(State)などを適切に追加してください。
# __init__.py(Azure Functions v2 以降の Python)
import json
import azure.functions as func
from botbuilder.core import (
CloudAdapter,
MessageFactory,
TurnContext,
ConfigurationServiceClientCredentialFactory,
)
from botbuilder.schema import Activity
credentials_factory = ConfigurationServiceClientCredentialFactory()
adapter = CloudAdapter(credentials_factory)
async def _logic(turn_context: TurnContext):
if turn_context.activity.type == "message":
text = turn_context.activity.text or ""
await turn_context.send_activity(MessageFactory.text(f"Echo: {text}"))
async def main(req: func.HttpRequest) -> func.HttpResponse:
body = req.get_body()
activity = Activity().deserialize(json.loads(body))
auth_header = req.headers.get("Authorization", "")
try:
await adapter.process_activity(activity, auth_header, _logic)
return func.HttpResponse(status_code=200)
except Exception as e:
# ここで adapter を使わずに裸で送ろうとすると 401 を誘発しやすい
return func.HttpResponse(str(e), status_code=500)
この構成で 返信 401 は発生しません。発生する場合は、ほぼ確実に資格情報・テナント・リージョンのいずれかがズレています。
よくある Q&A
Q. Graph の ChatMessage.Send や ChannelMessage.Send を付ければ直りますか?
A. 直りません。Bot Framework の返信経路は Graph ではなく Bot Service のコネクタです。Graph 権限は別用途(Graph 経由で Teams に投稿したい等)でのみ検討してください。
Q. マルチテナントで運用したいのですが?
A. ボットの認証は Single tenant で安定化しつつ、マルチテナント配布は「アプリの組織許可/インストール」側で対応するのが定石です。資格情報の配布・保護は Managed Identity や Key Vault を併用して運用負荷を抑えましょう。
Q. なぜ受信は成功するのに返信だけ失敗するのですか?
A. 受信は Bot Service → あなたの関数のサーバー間通信で、あなたの AAD トークンは不要。返信は あなた → Bot Service の呼び出しで、あなた側のトークンが必須だからです。
まとめ
「受信できるのに返信で 401」は、ボットのアイデンティティが正しく Bot Service に認められていないことのシグナルです。Graph 権限の付け足しは的外れ。Single tenant、AppId/Secret(または Managed Identity)、正しい Audience のトークン、そして リージョン整合――この 4 点を順番に固めれば、ほぼ確実に解消できます。現場ではまず CloudAdapter に寄せ、設定をミニマムにすることが最短ルートです。
追加のヒント・注意点
Teamwork.Migrate.All等の Graph 権限では Bot の返信はできません。- Bot Framework Dev Portal の API は UI 用です。返信用の認証トークンには関与しません。
- 将来、複数テナントで使うなら User‑Assigned Managed Identity の採用が無難。テナント差異を意識せず資格情報を安定供給できます。

コメント