こんにちは、ふくちです。
皆さん agentcore launch
してらっしゃいますでしょうか。
AgentCoreの発表から約1ヶ月が経ち、恐らく多くの方がAgentCoreデビューされたことと思います。
次なる一歩として、GatewayとIdentityを使いこなせるようになってみましょう!
この記事では、Slackを操作できるAIエージェントをAgentCore上へ構築してみます。
登場人物は以下の通りです。
- AgentCore Runtime
- この上でStrands Agentsが動作
- AgentCore Gateway
- これを経由してSlackへアクセスする
- AgentCore Identity
- RuntimeとGateway、GatewayとSlackの間で認証認可を確認します
- Slack
- あなたの好きなSlack Workspace(パブリックチャンネルが最低1つあればOK)
最終ゴールとしては、AIエージェントがSlackを操作できるようになること、その過程でAgentCore GatewayとIdentityの関係について理解することです。
実装パートと、解説パートをそれぞれ分けて進めます。
お好きな方から読んでください!
実装パート
事前準備:SlackのBot User OAuth Tokenを取得する
以下手順に従って、AgentCoreの設定時に必要な情報を取得しておきます。
ステップ1: Slackアプリの作成
- Slack APIにアクセス
- 「Create New App」をクリック
- 「From scratch」を選択
- アプリ名とワークスペースを設定
ステップ2: OAuth & Permissionsの設定
- 左側メニューから「OAuth & Permissions」を選択
- 「Scopes」セクションで必要なBot Token Scopesを追加
-
chat:write
- メッセージ送信 -
channels:read
- チャンネル情報の読み取り -
channels:history
- チャンネル履歴の読み取り -
files:write
- ファイルアップロード -
users:read
- ユーザー情報の読み取り
-
ステップ3: Bot User OAuth Tokenの取得
- 「OAuth & Permissions」セクションにおいて、Bot User OAuth Token(
xoxb-
から始まるトークン)をコピーする
IdentityでOutbount Authの作成
AgentCoreコンソールからIdentityを作成していきます。
これはこの後作成する、Gateway TargetへのOutbound Authとして機能します。
以下を選択した後、画面右下の Add OAuth Client
をクリックして作成を進めます。
- Outbound Auth:
Add API Key
- Name:
slack-bot-token
- API Key:
上記でコピーしたトークンを入力
Gatewayの作成
AgentCoreコンソールから、Gatewayを作成していきます。ここでは以下の領域を作成しています。
以下を設定してから、画面右下の Create gateway
をクリックします。
- Gateway name:
slack-gateway
- Inbound Auth Configuration:
Cognito
- IAM Permissions
- ✓
Use an IAM service role
- IAM role:
Create and use a new service role
- ✓
- Target
- Target name:
slack
- Target description:
access to my slack workspace
- Target type:
Integrations
- Integration provider:
Slack
- Select tool template:
Slack template
- Outbound Auth configurations:
API Key
- OAuth client:
slack-bot-token
- Target name:
- Additional configuration
- Location:
Header
- Parameter name:
Authorization
- Prefix:
Bearer
- Location:
Gateway作成完了後、Cognitoを確認すると、ユーザープールが1つ作成されています。
Inbound OAuth、つまりエージェントがGatewayへアクセスしてきた時の認証周りで用いられます。
ここでアプリケーションクライアントが作成されているのですが、これはM2M(Machine to Machine)認証という方式を用いるものになっており、ユーザーの介入なしでエージェントがGatewayへアクセスできるような仕組みになっています。
ここまでで、以下3つが作成できました。
- Gateway(エージェントが外部リソースへアクセスするためのドア)
- Gatewayのターゲット(Slackを指定)
- 外部向け認証(Gatewayを通ってターゲットへアクセスするための許可証)
Resource Credential Providerを作成する
最後に、Resource Credential Providerなるものを作成します。
簡単に言うと、AIエージェント・IDプロバイダー・リソースサーバー間の関係を管理する仲介役です。
図でいうとこの辺です。
これを使うことで、開発者はエージェントとGateway間における認証周りの複雑な設定を自分で実装しなくて済みます。
それでいて認証周りの管理と設定(ID管理・トークン管理・OAuth処理など)をAWS側に任せられるので、簡単にセキュリティを向上させられます。
これを作成するために、先ほど作成したGatewayに紐づくCognitoのクライアントID・クライアントシークレットが必要です。
このCognitoは先程Gatewayの Inbound OAuth
として作成したものですね。
また、GatewayのDiscovery URLも用意しておきます。
AgentCore Identityコンソールを開き、Outbound OAuth clientを作成していきます。
ここで上記3つの値を入力します。
- Outbound Auth:
Add OAuth client
- Name:
agentcore-identity-for-gateway
- Provider
- ✓
Custom provider
- ✓
- Provider configurations
- Configuration type: ✓ Discovery URL
- Client ID:
Cognitoで取得したクライアントID
- Client secret:
Cognitoで取得したクライアントシークレット
- Discovery URL:
GatewayのDiscovery URL
これでCredential Providerが作成できました。
※ここでもシークレットはSecrets Managerに保存されます。
ここまでで事前準備完了です。
Gatewayへアクセスするエージェントを構築する
最後に、AgentCore RuntimeへデプロイするAIエージェントを作っていきましょう。
このAIエージェントのツールとして、上記で作成したGateway(MCP)をツールとして与えてあげる形です。
詳細な実装な以下リポジトリに配置しております。
AgentCore Identityを使用してアクセストークンを取得する
以下ドキュメントを参考に、エージェントがGatewayへアクセスするためのアクセストークンを取得していきます。
ここからは上記リポジトリの実装を簡単に見ていきます。一部ログ出力やエラーハンドリングは省略しています。
まずはIdentityを用いたエージェントを作成するクラスを作成します。
このクラス内で、以下のことを実装しています。
- エージェントからGatewayへのアクセストークン取得
- MCPクライアント作成
- Gatewayとの接続確認(ツール確認)
- エージェントの作成
- エージェントの実行処理
まずはエージェントからGatewayへアクセスするためのトークン取得処理です。
@requires_access_token
というデコレータを用いることで、簡単にGatewayへのインバウンド認証設定が可能です。
# AgentCore Identityからアクセストークンを取得するためのデコレータをインポート
from bedrock_agentcore.identity.auth import requires_access_token
# MCPクライアントのインポート
from mcp.client.streamable_http import streamablehttp_client
class AgentWithIdentity:
"""
Cognito M2M認証を使用したAgentCore Identityを利用するエージェント。
"""
def __init__(self):
self.gateway_url = os.environ.get("GATEWAY_URL")
self.cognito_scope = os.environ.get("COGNITO_SCOPE")
self.workload_name = os.environ.get("WORKLOAD_NAME", "slack-gateway-agent")
self.region = region
async def get_access_token(self) -> str:
"""AgentCore Identityを使用してアクセストークンを取得する。
Runtime環境では、runtimeUserIdはInvokeAgentRuntime API呼び出し時に
システム側が設定し、Runtimeがエージェントに渡します。
Returns:
str: 認証されたAPIコール用のアクセストークン
"""
# @requires_access_tokenデコレータ付きのラッパー関数を作成
# Runtime環境では、デコレータが内部で_get_workload_access_tokenを呼び出し、
# workload access tokenを自動的に取得する
@requires_access_token(
provider_name="agentcore-identity-for-gateway",
scopes=[self.cognito_scope],
auth_flow="M2M",
force_authentication=False,
)
async def _get_token(*, access_token: str) -> str:
"""
AgentCore Identityからアクセストークンを受け取る内部関数。
デコレータが内部で以下を処理:
1. _get_workload_access_tokenを呼び出してworkload access tokenを取得
- workload_name: Runtime環境から取得
- user_id: InvokeAgentRuntimeのruntimeUserIdヘッダーから取得
2. workload access tokenを使用してOAuth tokenを取得
3. access_tokenパラメータとして注入
Args:
access_token: OAuthアクセストークン(デコレータによって注入)
Returns:
str: APIコールで使用するアクセストークン
"""
return access_token
# デコレータ付き関数を呼び出してトークンを取得
return await _get_token()
後続で、取得したアクセストークンを元に、GatewayへアクセスするMCPクライアントを作成する関数を定義します。
このMCPクライアントを元に、Gatewayと通信することになります。
async def access_to_slack(self, payload: Dict[str, Any]):
"""
完全なフロー: トークン取得 → エージェント作成 → ストリーミングでSlackワークスペースにアクセス。
これはAgentCore Identityの推奨される2ステップパターンを示しています:
1. @requires_access_tokenを使用してアクセストークンを取得
2. トークンを使用して認証されたクライアントを作成し、操作を実行
Args:
payload: ユーザープロンプトを含むAgentCore Runtimeペイロード
Yields:
エージェントからのストリーミングレスポンスイベント
"""
# ステップ1: AgentCore Identityを使用してアクセストークンを取得
access_token = await self.get_access_token()
# ステップ2: 認証されたMCPクライアントでエージェントを作成
def create_streamable_http_transport():
"""
Bearerトークン認証を使用したストリーミング可能なHTTPトランスポートを作成。
このトランスポートは、MCPクライアントがGatewayへの認証された
リクエストを行うために使用されます。
"""
transport = streamablehttp_client(
self.gateway_url,
headers={"Authorization": f"Bearer {access_token}"}
)
return transport
def get_full_tools_list(client):
"""
ページネーションをサポートしてすべての利用可能なツールをリスト。
Args:
client: MCPクライアントインスタンス
Returns:
list: 利用可能なツールの完全なリスト
"""
more_tools = True
tools = []
pagination_token = None
while more_tools:
tmp_tools = client.list_tools_sync(pagination_token=pagination_token)
tools.extend(tmp_tools)
if tmp_tools.pagination_token is None:
more_tools = False
else:
more_tools = True
pagination_token = tmp_tools.pagination_token
return tools
# 認証されたトランスポートでMCPクライアントを作成
mcp_client = MCPClient(create_streamable_http_transport)
その後、作成したMCPクライアントとGatewayのセッションが有効な場合に、エージェントを作成します。
ツールとしては、Gateway経由で使える組み込みのSlack操作ツールを与えます。
try:
with mcp_client:
# ステップ3: 認証された接続を通じて利用可能なツールをリスト
tools = get_full_tools_list(mcp_client)
try:
tools_names = [getattr(tool, 'tool_name', getattr(tool, 'name', str(tool))) for tool in tools]
except Exception as e:
logger.warning(f"ツール名の取得に失敗: {e}")
tools_names = [str(tool) for tool in tools]
# ステップ4: 認証されたツールでエージェントを作成
agent = Agent(
tools=tools,
model="us.anthropic.claude-sonnet-4-20250514-v1:0",
system_prompt=
"""
あなたはSlack統合アシスタントです。
以下の操作が可能です:
- チャンネル一覧の取得と検索
- メッセージの送信(チャンネルまたはスレッド)
- チャンネル履歴の取得
- ユーザー情報の確認
ユーザーのリクエストを理解し、適切なSlack操作を実行してください。
操作結果は明確に報告してください。
"""
)
# ステップ5: ストリーミングでSlackワークスペースにアクセス
user_message = payload.get("prompt", "")
logger.info(f"ユーザーメッセージ: {user_message}")
# ストリーミングレスポンスを使用
agent_stream = agent.stream_async(user_message)
# ストリーミングイベントをyieldで返す
async for event in agent_stream:
# デバッグ用:ツール実行に関するイベントをログ出力
if isinstance(event, dict):
if event.get('current_tool_use'):
tool_info = event.get('current_tool_use')
logger.info(f"🔧 ツール実行中: {tool_info}")
elif event.get('delta') and event['delta'].get('toolUse'):
logger.info(f"🚀 ツール呼び出し開始: {event['delta']['toolUse']}")
elif 'data' in event and 'Tool #' in str(event.get('data', '')):
logger.info(f"📋 ツール情報: {event['data']}")
yield event
logger.info(f"Slackへのアクセス完了")
except Exception as e:
#以下略
最終的に、先ほど作成した AgentWithIdentity
クラスを用いてインスタンスを作成し、ストリーミングでレスポンスを返します。
# AgentCoreアプリケーションを初期化
app = BedrockAgentCoreApp()
@app.entrypoint
async def slack_agent(payload: Dict[str, Any]):
"""Slackツール連携エージェントのメインエントリーポイント
Args:
payload: AgentCore Runtimeから渡されるペイロード
- prompt: ユーザーからの入力メッセージ
Yields:
AgentCore Runtime形式のストリーミングレスポンス
"""
# AgentWithIdentityインスタンスを作成
agent_with_identity = AgentWithIdentity()
try:
# ストリーミングレスポンスを転送
async for event in agent_with_identity.access_to_slack(payload):
# エラーイベントの場合はそのまま返す
if "error" in event:
yield event
# データイベントの場合は適切な形式で返す
elif "data" in event:
yield event
# その他のイベント(ツール使用など)もそのまま返す
else:
yield event
except Exception as e:
# 以下略
if __name__ == "__main__":
app.run()
AgentCore Runtimeへデプロイする
ここまでできたら、以下コマンドを実行してデプロイします。
1つだけ注意点としては、今回の実装上、agentcore launchコマンドで環境変数を設定する必要があります。
- GATEWAY_URL:
Gateway resource URL
に記載されているURL - COGNITO_SCOPE:
Cognito アプリケーションクライアントのカスタムスコープ
# uvで仮想環境を起動しておく
$ source .venv/bin/activate
$ agentcore configure --entrypoint slack_gateway_agent.py -er <AgentCore RuntimeサービスロールARN>
$ agentcore launch \
--env GATEWAY_URL=https://*************** \
--env COGNITO_SCOPE=************
ここで設定した環境変数は、Runtimeコンソールで Versions
から確認できます。
動作確認
以下コマンドを実行することで、Slackをエージェントが操作してくれます。
まずはチャンネル読み取り。
$ agentcore invoke '{"prompt": "Slackのチャンネル一覧を取得して"}' \
--agent slack_gateway_agent \
--user-id "m2m-user-001"
# 回答例
{
"result": {
"type": "AgentResult",
"stop_reason": "end_turn",
"message": {
"role": "assistant",
"content": [
{
"text": "Slackのチャンネル一覧を取得しました。現在、以下の2つのチャンネルがあります:\n\n## チャンネル一覧\n\n1. **create-app** (ID: ********)\n- プロジェクト用チャンネル\n- 目的:create-appプロジェクトに関する議論、ミーティング開催、資料共有\n- メンバー数:2人\n\n2. **test-strands-agents** (ID: ********) ⭐ **一般チャンネル**\n- 目的:会社のニュース、今後のイベント、称賛に値するチームメンバーの最新情報を共有\n- 社内ハンドブックのCanvasタブ付き\n- メンバー数:1人\n\n全てのチャンネルがパブリックチャンネルで、アーカイブされていません。各チャンネルには目的に応じたCanvasやタブが設定されているものもあります。"
}
]
}
}
}
メッセージ送信もしてもらいましょう。メンションもつけてもらったりして。
$ agentcore invoke '{"prompt": "test-strands-agentsチャンネルに、こんばんはとメッセージを送信して"}' \
--agent slack_gateway_agent \
--user-id "m2m-user-001"
$ agentcore invoke '{"prompt": "test-strands-agentsチャンネルへ、haruki-fukuchiさんが貴方を招待してくれました。haruki-fukuchiさんにメンションをつけてありがとうと言って。"}' \
--agent slack_gateway_agent \
--user-id "m2m-user-001"
実行結果がこちら!(チャンネルへの招待は先にしておいてください)
(まだこのミーム味するのかな)
これで、Slackを操作できるAIエージェントが爆誕しました!
解説パート
ここからは登場人物たちの関係性と、認証認可周りの話をしていきます。
登場人物のおさらいと関係の図示
登場人物を再掲しておきます。
- AgentCore Runtime
- この上でStrands Agentsが動作
- AgentCore Gateway
- これを経由してSlackへアクセスする
- AgentCore Identity
- RuntimeとGateway、GatewayとSlackの間で認証認可を確認します
- Slack
- あなたの好きなSlack Workspace(パブリックチャンネルが最低1つあればOK)
ここに、認証認可の要素を加えてみたいと思います。
- AgentCore Runtime
- この上でStrands Agentsが動作
-
Gatewayに入るためのJWT(Cognito/OIDCのアクセストークン)を使用
→今回agentcore-identity-for-gateway
として作成したIdentityを使用
→コード内で@requires_access_token
を使って実装した部分
- AgentCore Gateway
- これを経由してSlackへアクセスする
-
入口でエージェントのJWT(JSON Web Token)を検証
→今回agentcore-identity-for-gateway
として作成したIdentityを使用
→Gateway自動作成されたCognitoを使用 -
出口でSlackに投げるHTTPを作り、Bearerトークンを自分でつける
→今回slack-bot-token
として作成したIdentityを使用
→Outbound認証はエージェントではなく、Gateway側の仕事
→つまり、この部分はコード側の実装は不要
- AgentCore Identity
- RuntimeとGateway、GatewayとSlackの間で認証認可を確認します
-
GatewayからのOutbound認証について、Token Vault/API Key Credential Providerを自動で活用
→Botトークンxoxb-
を安全に保管し、Gatewayが必要なときだけ取り出す仕組み
- Slack
- あなたの好きなSlack Workspace(パブリックチャンネルが最低1つあればOK)
Authorization: Bearer xoxb-...
を検証(スコープ・インストール状態・ワークスペース一致など)し、JSONを返す
ということで図にすると、こんな感じでしょうか。
重要なのは、Runtime(エージェント)からGatewayで1段階目の認証があり、GatewayからSlackで2段階目の認証がある、ということです。
1リクエストの裏側を具体化
事前設定およびRuntimeへのデプロイが完了した状態で、agentcore invokeをした際の流れを見ていきましょう。
図示すると以下の流れです。
Token Vaultくんが突然出てきた感じがしますが、こいつはAgentCore Identityの内部でトークンを保管してくれる金庫みたいなものです。
以降、この図に沿って内部を見ていきましょう。
①agentcore invokeで呼び出し開始
- 以下コードの部分で、AgentCore IdentityからGateway入口用JWTを取得する
slack_gateway_agent.py
@requires_access_token( provider_name="agentcore-identity-for-gateway", scopes=[self.cognito_scope], auth_flow="M2M", force_authentication=False, )
- 以下コードの部分で、MCPクライアントを作成する
slack_gateway_agent.py
def create_streamable_http_transport(): transport = streamablehttp_client( self.gateway_url, headers={"Authorization": f"Bearer {access_token}"} ) return transport # 認証されたトランスポートでMCPクライアントを作成 mcp_client = MCPClient(create_streamable_http_transport)
- エージェントがプロンプトを解釈して、必要であればMCPのtoolUseを選択する
②Gatewayの入口でInbound認証
- Gatewayは CUSTOM_JWT Authorizerで、エージェントのJWTを検証
-
discoveryUrl
の JWKS で署名検証 -
allowedClients / aud / scope
をチェック - OK なら MCP
tools/call
を受理
-
③ツール解決とリクエスト生成
- GatewayはTargetとなるSlack templateのOpenAPIから該当するoperationを引く
- HTTPリクエストを新規作成
- URL:
https://slack.com/api/conversations.list
など - メソッド/クエリ/ボディはOpenAPI+ツール入力に従う(ここは見えない)
- Outbound認証を注入する
- URL:
- GatewayはTargetに紐づいたCredential Providerを確認
- 種類:
API Key
- 取り出し先: Token Vault(
xoxb-...
) - 注入場所:
HEADER
- パラメータ名:
Authorization
- Prefix:
Bearer
- 最終ヘッダー:
Authorization: Bearer xoxb-...
- 種類:
- これでSlack APIをコールする
④Slack側の検証と応答
- Slackは
xoxb-...
が有効か検証する- スコープ不足:
missing_scope
エラー - トークン不正/ヘッダー崩れ:
invalid_auth
- スコープ不足:
- jsonを返す (ok:
true
orfalse
)
⑤Gateway→エージェント→ユーザー
- Gatewayは
toolResult
としてエージェントに返し、自然言語に整形 - agentcore invoke のストリームで最終回答が戻る
セキュリティ面でのメリット
AgentCore GatewayとIdentityを使うことで、セキュリティ的に嬉しいポイントがいくつかあります。
- Token Vault:xoxb-... は API Key Provider としてVaultに保存
→コード側での実装不要+トークンのお漏らしが無くなる - Inbound と Outbound を分離
Inbound(Gateway入口)=JWT(Cognito/OIDC)
Outbound(Slack)=API Key Provider(xoxb-...)
→それぞれ独立して管理・ローテーション可能
今回はM2MでのIdentity設定でしたが、USER_FEDERATIONにすれば、利用者の情報に基づいてアクセス可能なツールなどを制限することも可能です。
余談:認証認可で出てくるキーワード
私この認証認可辺りをほとんど理解できていないので、GPT-5, Opus4.1に聞きながら用語について纏めてみました。
間違いがあればご指摘ください。
JWTとは
JSON Web Tokenの略で、署名付きの小さなJSONを指します。
具体的には、ヘッダー・ペイロード(本文)・署名の3つをドットで繋いだ文字列。
headerhogehoge123.payloadfugafuga456.signpikopiko789
ここでは、Cognito/OIDCのアクセストークン=JWTを指します。
Runtime上で動くエージェントがJWTを Authorization: Bearer <JWT>
でGatewayに渡し、そこでInbound認証を行います。
(逆に考えても良いかも知れません。GatewayのInbound認証でCognitoを選択したため、内部的にはCognitoが検証します。つまりJWTがCognitoのアクセストークンである、ということです。)
このJWT認証は、文字通りトークンベースの認証です。つまり、アプリケーションがユーザーのログイン状態を保持しない、ということです。
送信側は毎回のHTTPリクエストを行う際にJWTを一緒に送り、受け取り側は毎回そのJWTが有効かどうかを検証する、という流れです。
AWSのマネジメントコンソールのようにユーザーのログイン情報をアプリケーション側で保持しておくようなセッションベースの認証とは異なる、ということです。
これによってアプリケーション側の認証をシンプルな仕組みにしつつ、JWTが改ざんされていないかをリクエストごとに確認できる、というわけです。
もっと詳しく知りたい場合は、以下のブログがわかりやすかったのでおすすめです。
OIDCとは?
OpenID Connectの略で、ログインの標準規格です。
クライアントアプリからのIDトークン要求と、その応答を標準化したものが該当します。
※この図は以下ブログに掲載されていたものです。ブログを読んだほうがわかりやすいので、気になる方はそちらをご参照ください。
ワークロードアクセストークン(WAT)とは
ここまで単語としては出てきていませんが、この後のトラブルシューティングメモで出てくるのでこちらで解説します。
このワークロードアクセストークンとは、AgentCoreの中だけで通用する通行手形みたいなものです。
具体的には「誰が、どのエージェント(=ワークロード)として、どんなアクションを許可されているか」を束ねたスコープ付きのトークンです。
これを持っていると、IdentityのToken Vault(Credential Provider)から資格情報を取り出すなど、AgentCore内部での特権操作が可能になります。
なぜ必要かというと、AgentCore内部でVaultからAPI KeyやOAuthトークンを取り出すためには、AgentCoreが発行した用途限定の鍵が必要だからです。
先程出てきたJWTはAgentCore外での身分証、WATはAgentCore内部での代理人身分証、なイメージです。
なので、WATはRuntime・Gateway・Identity・Token Vaultといった内部でのやり取りにのみ使用し、Slackなど外向けの通信には使いません(Bot Tokenを使う)。
一覧表はこんな感じです。
項目 | ユーザーJWT(Cognito等) | workload access token(WAT) |
---|---|---|
役割 | 外の世界の身分証(誰か) | AgentCore 内の委任鍵(誰が×どのワークロードで×何を) |
使い道 | Runtime → Gateway 入り口認証 / Runtime への持ち込み | VaultやAgentCore内部APIへの認可 |
寿命/範囲 | IdP次第(短め)/ 外部サービスで通用 | もっと短命 / AgentCore 内だけ |
漏洩時 | 外部APIに誤用リスク | AgentCore内でのみ悪用可能(短命&スコープ制限) |
WATの作り方などについてはトラブルシューティングメモで解説しています。
それぞれの認証の違い
今回はRuntimeとGateway、GatewayとSlackで2段階の認証になっている、というのはご認識いただいているかと思います。
ただ、それぞれ異なる認証方法を用いているので、個別に解説していきます。
おさらいですが、全体の流れとしてはこんな感じです。
ここから、Runtime→Gatewayの認証と、Gateway→Slackの認証について、それぞれ個別に解説していきます。
Runtime→Gatewayの認証:公開鍵方式のJWT
- 誰が署名?: Cognito
- どう検証?: Gatewayが公開鍵(JWKS)を取得・キャッシュして署名を検証
- 何をチェック?:
iss
(発行者),aud
(クライアントID/許可先),exp
(有効期限),scope
など - 意味合い: 改ざん検知と発行者の正当性で「あなたは正規の呼び出し元です」を証明
RuntimeとGatewayでの認証ではOpenID Connectが用いられています。
ただし、今回はM2M認証なので、ユーザーが事実上存在しません。Runtime上のエージェントが認証リクエストしているためですね。代わりにリソースサーバーが存在します。
- クライアントアプリ(便宜上のユーザー):
Runtime(エージェント)
- OpenIDプロバイダー:
Cognito(トークン発行者)
- リソースサーバー:
Gateway
ここで、RuntimeがCognitoからJWTを取得し、それをGatewayの入口で提示する、という流れが少し見えてくるかなと思います。
Gateway→Slackの認証:共通鍵的なAPIキー認証
- 何を送る?: Authorization: Bearer xoxb-…(SlackのBotトークン)
- どう判定?: Slack 側がトークンを見て、その権限(スコープ)と所有ワークスペースを突き合わせて許可
- 意味合い: 「鍵そのものを見せる」ので、持っていれば通れる(盗難耐性は運用で担保)
GatewayとSlackの認証では、共通鍵的(Bearer Token)なAPIキー認証を行っています。
Identityに登録した xoxb-...
というSlackのBot Tokenを共有の秘密鍵そのものとして扱います。
これをAuthorization: Bearer
で送って認証を行います。
Runtime→Gatewayと異なるのは、Cognitoなどを使わず、鍵を直接使用しているという点です。
鍵を直接使用するのは危険な感じもしますが、Identityに登録すればSecrets Managerで保存され、必要なときにのみ使用されるため、セキュリティ的にはそこまでのBad Practiceでは無いと(個人的には)解釈しています。
よくありそうな疑問
- MCPClientがSlackとやり取りするの?
- No、ここでMCPClientはRuntime上のエージェントなので、Gatewayとのみ通信します
- コード側でSlackのBot Tokenを取得しなくて良いの?
- 取得しなくてOK、Gatewayが自動でやってくれるため
- そもそも今回作成するコードはRuntime上で動作するエージェントの定義なので、Slackには直接接続しない
他にもあったら教えて下さい!
開発時のトラブルシューティングメモ
ここまで実装するのに、山程トラブルシューティングしたので、そのメモを残しておきます。
同じミスに引っかかる方が少しでも減れば幸いです。
「Workload access token has not been set」エラー
これは以下のように、ただ普通に agentcore invoke
を実行すると起こります。
$ agentcore invoke '{"prompt": "Slackのチャンネル一覧を取得して"}'
# 実行結果
{"error": "リクエストの処理中にエラーが発生しました: Workload access token has not been set."}
agentcore invoke
コマンド実行時に、--user-id
を指定することで解決できます。
$ agentcore invoke '{"prompt": "Slackのチャンネル一覧を取得して"}' \
--agent slack_gateway_agent \
--user-id "m2m-user-001"
その理由は、ワークロードアクセストークンを作成する際に必要なパラメータだからです。
前提として、今回はToken VaultからAPI Keyを取得しなければいけない関係上、必ずWATを作成することが求められます。
@requires_access_token
というデコレータを用いていた部分ですね。
これによって裏側でWATをする際、「誰のリクエストか」を手がかりにWATを発行します。
誰かを識別するために、user_id
か ユーザーのJWT
のどちらかが必要になります。
- 普通に
agentcore invoke
したときのことを考えてみます- この時、「誰」を識別するための情報が与えられていない
- WATを作成することができない
- Token Vault取得時に「Workload access token has not been set」というエラーを返す
-
--user-id
を設定したときのことを考えてみます-
runtimeUserId = m2m-user-001
という「誰」の情報を受け取る - workload、runtimeUserIdの情報を下にWATを作成する
- Gateway/IdentityがこのWATを提示してToken VaultからAPI Keyを取得
-
という形になっていたようです。
Credential Providerを使うツールの場合WATは必須のため、他の外部リソースと接続する時などにも必要になります。
手っ取り早く動かしたいときは、これで良さそうです。
ただ実際のアプリケーションのことを考えると、ユーザーJWTをアプリ側で取得し、それをリクエストに含めるような形のほうが良さそうです。
ここは引き続き調査してみます。
「Invocation failed: Read timeout on endpoint URL: "None"」エラー
これはエージェントの呼び出し自体は成功している状態です。
ただ、ToolUseのところでタイムアウトエラーが返ってきています。
$ agentcore invoke '{"prompt": "Slackのチャンネル一覧を取得して"}' \
--agent slack_gateway_agent \
--user-id "m2m-user-001"
# サンプルレスポンス
{
"message": {
"role": "assistant",
"content": [
{
"text": "Slackのチャンネル一覧を取得します。"
},
{
"toolUse": {
"toolUseId": "tooluse_*************",
"name": "slack___conversationsList",
"input": {}
}
}
]
}
}
❌ Invocation failed: Read timeout on endpoint URL: "None"
事象としては、エージェントがtoolUseをコールした後、Gateway→Slackで応答が返らないまま、Gatewayのread-timeout(55秒)になっています。
これの原因としては、Identityに設定したAPI Keyが誤っている、もしくはOAuthの方でIdentityに設定してしまっていることが考えられます。
私の場合はOAuthでやろうとしていたのですがここで一生失敗し続け、API Keyに変えてみると上手く動作するようになりました。
なぜこんなことになるかというと、OAuthだとユーザー同意型(USER_FEDERATION)の認証が必要なのですが、今回はM2Mの認証でやろうとしてしまい、そこで不整合が起こっていたのでは…と考えています(恐らく).
M2M認証の場合はAPI KeyでIdentityを作成する必要があるとのことだったので、こちらにすることで上手く動作しました。
その際、Headerに Authorization: Bearer xoxb-...
を付与する必要があるため、冒頭のGatewayの設定で上記を追加するようにしていました。
Slackから{"ok": false, "error": "invalid_auth"}が返ってくる
先程のAPI Key設定でGateway→Slackへの認証は上手くいきました。
ただ、正しい認証情報が付与されていないとこのエラーが返ってきます。
ありそうなケースとしては、ヘッダーがついていない/ヘッダー設定を誤っている/Gatewayの設定が誤っている、などがあります。
特にヘッダー設定誤りが無いかは要確認です。余計なスペースが入っているとかは気づきにくいので要注意です(私は1日溶かしました)。
- Additional configuration
- Location:Header
- Parameter name:Authorization
- Prefix:Bearer
←スペース入ってるとエラーになります
エラーの切り分け方法
以下、エラーの切り分けに使えるコマンドを用意しておきます。困った方はお使いください。
- トークンが正しいか確認するためのコマンド
ターミナル
# 環境変数にSlack Bot Tokenを設定する $ export SLACK_BOT_TOKEN=xoxb-... # curlコマンドでauth.testへアクセス確認を行う curl -sS -H "Authorization: Bearer $SLACK_BOT_TOKEN" https://slack.com/api/auth.test # → {"ok":true,...} が正解
- httpbin でヘッダをエコーするターゲットを一時作成(Gatewayは作成済みの前提)
ターミナル
# OpenAPIスキーマファイルを作成 $ cat > httpbin-openapi.json <<'JSON' { "openapi":"3.0.0","info":{"title":"Echo","version":"1.0"}, "servers":[{"url":"https://httpbin.org"}], "components":{"securitySchemes":{"slackApiKey":{"type":"apiKey","in":"header","name":"Authorization"}}}, "paths":{"/anything":{"get":{"operationId":"echoHeaders","security":[{"slackApiKey":[]}],"responses":{"200":{"description":"OK"}}}}} } JSON $ jq -n --rawfile spec httpbin-openapi.json \ '{mcp:{openApiSchema:{inlinePayload:$spec}}}' > httpbin-target-config.json # 既存のGatewayにターゲットを追加作成する $ aws bedrock-agentcore-control create-gateway-target \ --gateway-identifier "<gateway-id or ARN>" \ --name "httpbin-echo" \ --description "diag auth header" \ --target-configuration file://httpbin-target-config.json \ --credential-provider-configurations '[ { "credentialProviderType":"API_KEY", "credentialProvider":{"apiKeyCredentialProvider":{ "providerArn":"<your-api-key-provider-arn>", "credentialLocation":"HEADER", "credentialParameterName":"Authorization", "credentialPrefix":"Bearer" }} } ]' \ --region us-east-1 # echoHeadersを実行してヘッダーを確認する $ agentcore invoke '{"prompt": "httpbin-echo___echoHeaders を実行して、返ってきたJSONのheadersだけを返して"}' --agent <agent-name> --user-id <user>
まとめ
オレオレAIエージェントに色んな機能を足しつつ、認証認可についても理解を深めていきましょう!
参考文献