13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Bedrock AgentCore GatewayとIdentityを使ってSlackへアクセスしてみる

Posted at

こんにちは、ふくちです。
皆さん 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アプリの作成

  1. Slack APIにアクセス
  2. 「Create New App」をクリック
  3. 「From scratch」を選択
  4. アプリ名とワークスペースを設定

ステップ2: OAuth & Permissionsの設定

  1. 左側メニューから「OAuth & Permissions」を選択
  2. 「Scopes」セクションで必要なBot Token Scopesを追加
    • chat:write - メッセージ送信
    • channels:read - チャンネル情報の読み取り
    • channels:history - チャンネル履歴の読み取り
    • files:write - ファイルアップロード
    • users:read - ユーザー情報の読み取り

ステップ3: Bot User OAuth Tokenの取得

  1. 「OAuth & Permissions」セクションにおいて、Bot User OAuth Token(xoxb-から始まるトークン)をコピーする

IdentityでOutbount Authの作成

AgentCoreコンソールからIdentityを作成していきます。
これはこの後作成する、Gateway TargetへのOutbound Authとして機能します。
image.png

以下を選択した後、画面右下の Add OAuth Client をクリックして作成を進めます。

  • Outbound Auth: Add API Key
  • Name: slack-bot-token
  • API Key: 上記でコピーしたトークンを入力

image.png

ここで設定したトークンは、Secrets Managerに自動で保存されます。
一度保存したシークレットの値を更新したい場合は、IdentityのEditボタンから行います
(Secrets Managerコンソールからは変更できない)
image.png

Gatewayの作成

AgentCoreコンソールから、Gatewayを作成していきます。ここでは以下の領域を作成しています。
image.png

以下を設定してから、画面右下の 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
  • Additional configuration
    • Location: Header
    • Parameter name: Authorization
    • Prefix: Bearer

image.png

image.png

image.png

Gateway作成完了後、Cognitoを確認すると、ユーザープールが1つ作成されています。
Inbound OAuth、つまりエージェントがGatewayへアクセスしてきた時の認証周りで用いられます。
image.png

ここでアプリケーションクライアントが作成されているのですが、これはM2M(Machine to Machine)認証という方式を用いるものになっており、ユーザーの介入なしでエージェントがGatewayへアクセスできるような仕組みになっています。
image.png

ここまでで、以下3つが作成できました。

  • Gateway(エージェントが外部リソースへアクセスするためのドア)
  • Gatewayのターゲット(Slackを指定)
  • 外部向け認証(Gatewayを通ってターゲットへアクセスするための許可証)

Resource Credential Providerを作成する

最後に、Resource Credential Providerなるものを作成します。

簡単に言うと、AIエージェント・IDプロバイダー・リソースサーバー間の関係を管理する仲介役です。
図でいうとこの辺です。
image.png

これを使うことで、開発者はエージェントとGateway間における認証周りの複雑な設定を自分で実装しなくて済みます。
それでいて認証周りの管理と設定(ID管理・トークン管理・OAuth処理など)をAWS側に任せられるので、簡単にセキュリティを向上させられます。

これを作成するために、先ほど作成したGatewayに紐づくCognitoのクライアントID・クライアントシークレットが必要です。
このCognitoは先程Gatewayの Inbound OAuth として作成したものですね。
image.png

この後のエージェントを構築するフェーズで、アプリケーションクライアント→ログインページタブ配下にある、カスタムスコープを用います。これもコピーしておいてください。
image.png

また、GatewayのDiscovery URLも用意しておきます。
image.png

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

image.png

これでCredential Providerが作成できました。
※ここでもシークレットはSecrets Managerに保存されます。

ここまでで事前準備完了です。

Gatewayへアクセスするエージェントを構築する

最後に、AgentCore RuntimeへデプロイするAIエージェントを作っていきましょう。
このAIエージェントのツールとして、上記で作成したGateway(MCP)をツールとして与えてあげる形です。

詳細な実装な以下リポジトリに配置しております。

AgentCore Identityを使用してアクセストークンを取得する

以下ドキュメントを参考に、エージェントがGatewayへアクセスするためのアクセストークンを取得していきます。

ここからは上記リポジトリの実装を簡単に見ていきます。一部ログ出力やエラーハンドリングは省略しています。

まずはIdentityを用いたエージェントを作成するクラスを作成します。
このクラス内で、以下のことを実装しています。

  • エージェントからGatewayへのアクセストークン取得
  • MCPクライアント作成
  • Gatewayとの接続確認(ツール確認)
  • エージェントの作成
  • エージェントの実行処理

まずはエージェントからGatewayへアクセスするためのトークン取得処理です。
@requires_access_token というデコレータを用いることで、簡単にGatewayへのインバウンド認証設定が可能です。

slack_gateway_agent.py
# 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と通信することになります。

slack_gateway_agent.py
    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操作ツールを与えます。

slack_gateway_agent.py
        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 クラスを用いてインスタンスを作成し、ストリーミングでレスポンスを返します。

slack_gateway_agent.py
# 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 から確認できます。
image.png

動作確認

以下コマンドを実行することで、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"

実行結果がこちら!(チャンネルへの招待は先にしておいてください)
image.png
(まだこのミーム味するのかな)

これで、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段階目の認証がある、ということです。
image.png

1リクエストの裏側を具体化

事前設定およびRuntimeへのデプロイが完了した状態で、agentcore invokeをした際の流れを見ていきましょう。

図示すると以下の流れです。
Token Vaultくんが突然出てきた感じがしますが、こいつはAgentCore Identityの内部でトークンを保管してくれる金庫みたいなものです。

以降、この図に沿って内部を見ていきましょう。

①agentcore invokeで呼び出し開始

  1. 以下コードの部分で、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,
    )
    
  2. 以下コードの部分で、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)
    
  3. エージェントがプロンプトを解釈して、必要であればMCPのtoolUseを選択する

②Gatewayの入口でInbound認証

  1. Gatewayは CUSTOM_JWT Authorizerで、エージェントのJWTを検証
    • discoveryUrl の JWKS で署名検証
    • allowedClients / aud / scope をチェック
    • OK なら MCP tools/call を受理

③ツール解決とリクエスト生成

  1. GatewayはTargetとなるSlack templateのOpenAPIから該当するoperationを引く
  2. HTTPリクエストを新規作成
    • URL: https://slack.com/api/conversations.list など
    • メソッド/クエリ/ボディはOpenAPI+ツール入力に従う(ここは見えない)
    • Outbound認証を注入する
  3. GatewayはTargetに紐づいたCredential Providerを確認
    • 種類: API Key
    • 取り出し先: Token Vault(xoxb-...)
    • 注入場所: HEADER
    • パラメータ名: Authorization
    • Prefix: Bearer
    • 最終ヘッダー: Authorization: Bearer xoxb-...
  4. これでSlack APIをコールする

④Slack側の検証と応答

  1. Slackはxoxb-...が有効か検証する
    • スコープ不足: missing_scopeエラー
    • トークン不正/ヘッダー崩れ: invalid_auth
  2. jsonを返す (ok: true or false)

⑤Gateway→エージェント→ユーザー

  1. Gatewayは toolResult としてエージェントに返し、自然言語に整形
  2. agentcore invoke のストリームで最終回答が戻る

セキュリティ面でのメリット

AgentCore GatewayとIdentityを使うことで、セキュリティ的に嬉しいポイントがいくつかあります。

  1. Token Vault:xoxb-... は API Key Provider としてVaultに保存
    →コード側での実装不要+トークンのお漏らしが無くなる
  2. 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つをドットで繋いだ文字列。

サンプルJWT
headerhogehoge123.payloadfugafuga456.signpikopiko789

ここでは、Cognito/OIDCのアクセストークン=JWTを指します。
Runtime上で動くエージェントがJWTを Authorization: Bearer <JWT> でGatewayに渡し、そこでInbound認証を行います。

(逆に考えても良いかも知れません。GatewayのInbound認証でCognitoを選択したため、内部的にはCognitoが検証します。つまりJWTがCognitoのアクセストークンである、ということです。)
image.png

このJWT認証は、文字通りトークンベースの認証です。つまり、アプリケーションがユーザーのログイン状態を保持しない、ということです。
送信側は毎回のHTTPリクエストを行う際にJWTを一緒に送り、受け取り側は毎回そのJWTが有効かどうかを検証する、という流れです。

AWSのマネジメントコンソールのようにユーザーのログイン情報をアプリケーション側で保持しておくようなセッションベースの認証とは異なる、ということです。

これによってアプリケーション側の認証をシンプルな仕組みにしつつ、JWTが改ざんされていないかをリクエストごとに確認できる、というわけです。

もっと詳しく知りたい場合は、以下のブログがわかりやすかったのでおすすめです。

OIDCとは?

OpenID Connectの略で、ログインの標準規格です。
クライアントアプリからのIDトークン要求と、その応答を標準化したものが該当します。
※この図は以下ブログに掲載されていたものです。ブログを読んだほうがわかりやすいので、気になる方はそちらをご参照ください。
image.png

ワークロードアクセストークン(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 ←スペース入ってるとエラーになります

エラーの切り分け方法

以下、エラーの切り分けに使えるコマンドを用意しておきます。困った方はお使いください。

  1. トークンが正しいか確認するためのコマンド
    ターミナル
    # 環境変数に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,...} が正解
    
  2. 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エージェントに色んな機能を足しつつ、認証認可についても理解を深めていきましょう!

参考文献

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?