前回の記事では Agentを呼び出す際の認証を、 IAM Signv4 と Cognito を用いた OAuth と2種類の方法で試しました。この記事ではAgent が外部サービスにアクセスを行う(API呼び出しなど)の認証を行っていきます。
2LO と 3LO
Outbound で外部サービスのトークンを取得する方法には、大きく2つの OAuth 2.0 フローがあります。AgentCore Identity は、client credentials grant(2LO とも呼ばれる)と、authorization code grant(3LO とも呼ばれる)の両方をネイティブにサポートしています。
2LO(Two-Legged OAuth / M2M: Machine to Machine)
登場人物が2者(エージェントと、アクセス先のサービス)のフローです。ユーザーの同意画面は登場しません。2LO フローでは、エージェントはユーザーの操作を介さずに、リソースサーバーに対して自分自身として直接認証します。
「エージェント自身の権限」で動くのが本質です。たとえば、社内 API を叩く、自社のサービスアカウントとしてデータを取得する、といった特定のユーザーに紐づかないアクセスに向いています。裏側では OAuth の client credentials grant が使われます。
3LO(Three-Legged OAuth / ユーザー委譲)
登場人物が3者(ユーザー、エージェント、外部サービス)のフローです。ここに「ユーザーの同意」というステップが加わります。3LO フローは、外部サービスにあるユーザー固有のデータへアクセスするために、ユーザーの明示的な同意と認可を可能にします。
「エージェントが、ユーザーに代わって、ユーザーのデータにアクセスする」のが本質です。例えば「エージェントが私にかわって Google Drive を読む」などがそれにあたります。流れとしては、エージェントが同意用の URL を生成し、ユーザーがブラウザでその URL を開いて「許可」を押すと、初めてエージェントがそのユーザーのデータにアクセスできるようになります。裏側では OAuth の authorization code grant が使われます。
2LO(M2M) | 3LO(ユーザー委譲) | |
|---|---|---|
登場人物 | エージェント ↔ サービス | ユーザー ↔ エージェント ↔ サービス |
ユーザー同意 | 不要 | 必要(初回のみ、以降はVault活用) |
誰として動くか | エージェント自身 | ユーザーの代理 |
OAuth grant | client credentials | authorization code |
|
|
|
向く用途 | 社内API・サービスアカウント | 個人のDrive/メール/カレンダー |
さっそくやってみる
ではこの記事ではまず2LO (M2M)を試してみます。
この記事では第一回の記事で作成されたCognito ユーザープールを再利用しますので、前回の手順を終わらせておいてください。
1. Cognito の環境準備
setup_m2m.sh を作成します。
#!/bin/bash
set -e
# 第一回で作ったユーザープールIDを指定
export POOL_ID=us-west-2_Yskjectap
export REGION=us-west-2
# ドメインは AWS 全体で一意である必要があるため、サフィックスをランダム化
export DOMAIN_PREFIX="agentcore-m2m-$(head /dev/urandom | tr -dc a-z0-9 | head -c 8)"
# 1. リソースサーバーと独自スコープを作成
# identifier: API を表す識別子 / scope: read アクセスを表す独自スコープ
aws cognito-idp create-resource-server \
--user-pool-id "$POOL_ID" \
--identifier "my-api" \
--name "MyProtectedAPI" \
--scopes ScopeName=read,ScopeDescription="Read access" \
--region "$REGION" > /dev/null
# 2. ユーザープールドメインを作成(トークンエンドポイント有効化に必須)
aws cognito-idp create-user-pool-domain \
--user-pool-id "$POOL_ID" \
--domain "$DOMAIN_PREFIX" \
--region "$REGION" > /dev/null
# 3. M2M 用アプリクライアントを作成(client_credentials 対応・シークレット付き)
CLIENT_JSON=$(aws cognito-idp create-user-pool-client \
--user-pool-id "$POOL_ID" \
--client-name "M2MClient" \
--generate-secret \
--allowed-o-auth-flows "client_credentials" \
--allowed-o-auth-scopes "my-api/read" \
--allowed-o-auth-flows-user-pool-client \
--region "$REGION")
export M2M_CLIENT_ID=$(echo "$CLIENT_JSON" | jq -r '.UserPoolClient.ClientId')
export M2M_CLIENT_SECRET=$(echo "$CLIENT_JSON" | jq -r '.UserPoolClient.ClientSecret')
# トークンエンドポイント(ドメインベース。cognito-idp のパスとは別物)
export TOKEN_ENDPOINT="https://${DOMAIN_PREFIX}.auth.${REGION}.amazoncognito.com/oauth2/token"
echo "----------------------------------------"
echo "Domain prefix : $DOMAIN_PREFIX"
echo "Token endpoint: $TOKEN_ENDPOINT"
echo "M2M Client ID : $M2M_CLIENT_ID"
echo "M2M Secret : $M2M_CLIENT_SECRET"
echo "Scope : my-api/read"
echo "----------------------------------------"実行します。
chmod +x setup_m2m.sh
source setup_m2m.sh出力される以下の値をメモしておきます。
----------------------------------------
Domain prefix : agentcore-m2m-uy7ad0pl
Token endpoint: https://agentcore-m2m-uy7ad0pl.auth.us-west-2.amazoncognito.com/oauth2/token
M2M Client ID : 5od8etreqjlk8qpeq0bnpqbk3b
M2M Secret : qiaoj34u6iethoo7frs1rfn0bqvmv3o8384aqln4a224qpbou27
Scope : my-api/read
----------------------------------------ではトークンの取得をテストします。
curl -s -X POST "$TOKEN_ENDPOINT" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "${M2M_CLIENT_ID}:${M2M_CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=my-api/read" | jq以下の様な値が出れば成功です。
{
"access_token": "eyJraWQiOiJUUFJya1dNQXd5QUZPVDZBVnVCZC9oOGZJRVBDb0Rnc1R3ZXI5Qk1kaDZVPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI1b2Q4ZXRyZXFqbGs4cXBlcTBibnBxYmszYiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoibXktYXBpL3JlYWQiLCJhdXRoX3RpbWUiOjE3ODc0NjA3NjUsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20vdXMtd2VzdC0yX1lza2plY3RhcCIsImV4cCI6MTc4NzQ2NDM2NSwiaWF0IjoxNzg3NDYwNzY1LCJ2ZXJzaW9uIjoyLCJqdGkiOiJlOGYxYWE5OS04YmQ5LTQ2N2QtODFlYS1jYTdlYjRjZjAwMmQiLCJjbGllbnRfaWQiOiI1b2Q4ZXRyZXFqbGs4cXBlcTBibnBxYmszYiJ9.zxyeJvB4FDOAwJtjW4C3wpNzOTB3mQkXe6glqqcOLsBbuhyYLKi-aXf7XF1gMvvT6cUQ-yExWjVsG_TNuq5Cb24C74ou2F5SHmGkKSm9tN1w3jen5xyqM2n2c9E5RbXwd8lpeWGUxUBdYdom-ViO-HOK_lPGwyxdTfMIlkQVEVHN6BCHsAqIpM9hwbofP4ixRxqNMyAu2th7PnUzVqZ0Anoww1txVTG9L_DgSsN6rsWRTHZGz9Z3r3SFu-oOM9L_KmBilIz69Y0kcDwvql8N2R3g0jfKzpB0IG3xFwwNDMafAP2oTb0b2xluiS72SfNmHqt2ORiccrUqG8vk8SI3gQ",
"expires_in": 3600,
"token_type": "Bearer"
}2. テスト用APIの準備
protected_api.py を作成します。
"""
保護された自前 API(2LO デモ用)
Cognito が発行した M2M アクセストークンを検証し、正しければデータを返す。
"""
import json
import time
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
import jwt # PyJWT
from jwt import PyJWKClient
# ---- 設定:自分の環境に合わせる ----
REGION = "us-west-2"
POOL_ID = "us-west-2_Yskjectap"
EXPECTED_SCOPE = "my-api/read"
EXPECTED_CLIENT_ID = "5od8etreqjlk8qpeq0bnpqbk3b" # M2M Client ID
# ------------------------------------
ISSUER = f"https://cognito-idp.{REGION}.amazonaws.com/{POOL_ID}"
JWKS_URL = f"{ISSUER}/.well-known/jwks.json"
# 署名検証用の公開鍵(JWKS)を取得するクライアント
jwks_client = PyJWKClient(JWKS_URL)
def verify_token(token: str) -> dict:
"""トークンの署名・issuer・有効期限・スコープ・client_id を検証する"""
signing_key = jwks_client.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer=ISSUER,
# M2M アクセストークンには aud が無いので audience 検証は無効化
options={"verify_aud": False},
)
# スコープ検証(独自スコープが含まれているか)
scopes = claims.get("scope", "").split()
if EXPECTED_SCOPE not in scopes:
raise ValueError(f"required scope '{EXPECTED_SCOPE}' not present")
# client_id 検証(想定した M2M クライアントか)
if claims.get("client_id") != EXPECTED_CLIENT_ID:
raise ValueError("client_id mismatch")
return claims
class Handler(BaseHTTPRequestHandler):
def _send(self, code: int, body: dict):
payload = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(payload)
def do_GET(self):
if self.path != "/data":
self._send(404, {"error": "not found"})
return
auth = self.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
self._send(401, {"error": "missing bearer token"})
return
token = auth[len("Bearer "):]
try:
claims = verify_token(token)
except Exception as e:
self._send(403, {"error": f"token verification failed: {e}"})
return
# 検証成功:保護されたデータを返す
self._send(200, {
"message": "Hello from the protected API!",
"authorized_client": claims.get("client_id"),
"scope": claims.get("scope"),
"server_time": int(time.time()),
})
def log_message(self, *args):
pass # アクセスログを抑制
if __name__ == "__main__":
port = 8081
print(f"Protected API listening on http://localhost:{port}/data")
HTTPServer(("0.0.0.0", port), Handler).serve_forever()実行に必要なライブラリをインストールします。
pip install "PyJWT[crypto]"準備が出来たらAPIを起動します。
python protected_api.pyではこのままの状態でもう一つターミナルを開いて、以下の環境変数を設定します。それぞれの値は先ほどメモしたものに置き換えてください。
export TOKEN_ENDPOINT="https://agentcore-m2m-uy7ad0pl.auth.us-west-2.amazoncognito.com/oauth2/token"
export M2M_CLIENT_ID="5od8etreqjlk8qpeq0bnpqbk3b"
export M2M_CLIENT_SECRET="qiaoj34u6iethoo7frs1rfn0bqvmv3o8384aqln4a224qpbou27"
export REGION="us-west-2"
export POOL_ID="us-west-2_Yskjectap"
export DOMAIN_PREFIX="agentcore-m2m-uy7ad0pl"ではトークンを取得してみます。
# トークン取得
export TOKEN=$(curl -s -X POST "$TOKEN_ENDPOINT" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "${M2M_CLIENT_ID}:${M2M_CLIENT_SECRET}" \
-d "grant_type=client_credentials&scope=my-api/read" | jq -r '.access_token')
echo "len=${#TOKEN}" # 今度は数百の数字になるはず
# トークン有りで叩く → 200
curl -i http://localhost:8081/data -H "Authorization: Bearer ${TOKEN}"以下の様な出力がされれば完成です。
len=843
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.12.3
Date: Sun, 23 Aug 2026 05:00:24 GMT
Content-Type: application/json
{"message": "Hello from the protected API!", "authorized_client": "5od8etreqjlk8qpeq0bnpqbk3b", "scope": "my-api/read", "server_time": 1787461224}3. AgentCore Identity の設定
では2番目のターミナルで以下を実行します。
aws bedrock-agentcore-control create-oauth2-credential-provider \
--name "cognito-m2m-provider" \
--credential-provider-vendor "CustomOauth2" \
--oauth2-provider-config-input "{
\"customOauth2ProviderConfig\": {
\"clientId\": \"${M2M_CLIENT_ID}\",
\"clientSecret\": \"${M2M_CLIENT_SECRET}\",
\"oauthDiscovery\": {
\"authorizationServerMetadata\": {
\"issuer\": \"https://cognito-idp.${REGION}.amazonaws.com/${POOL_ID}\",
\"authorizationEndpoint\": \"https://${DOMAIN_PREFIX}.auth.${REGION}.amazoncognito.com/oauth2/authorize\",
\"tokenEndpoint\": \"${TOKEN_ENDPOINT}\",
\"responseTypes\": [\"token\"]
}
}
}
}" \
--region "${REGION}"もどってきたARNの環境変数にセットします。
export CRED_PROVIDER_NAME="cognito-m2m-provider"
grep -q 'CRED_PROVIDER_NAME' env.sh || echo 'export CRED_PROVIDER_NAME="cognito-m2m-provider"' >> env.shこれまでは手で curl して取っていた M2M トークン取得していましたが、AgentCore Identity に丸ごと任せるコードに書き換えます。エージェントは「トークンをくれ」と言うだけで、取得・キャッシュ・注入は SDK と AgentCore がやってくれます。
agentcore_m2m.py を作成します。
"""
2LO Outbound デモ用エージェント。
AgentCore Identity から M2M トークンを取得し、保護 API を叩く。
"""
import urllib.request
import json
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.identity.auth import requires_access_token
app = BedrockAgentCoreApp()
# 保護 API のエンドポイント(Step 2 のサーバー)
PROTECTED_API_URL = "http://localhost:8081/data"
@requires_access_token(
provider_name="cognito-m2m-provider", # Step 3 で登録した名前
scopes=["my-api/read"], # 独自スコープ
auth_flow="M2M", # 2LO(client credentials)
)
async def call_protected_api(*, access_token: str) -> str:
"""AgentCore から注入された M2M トークンで保護 API を叩く"""
req = urllib.request.Request(
PROTECTED_API_URL,
headers={"Authorization": f"Bearer {access_token}"},
)
with urllib.request.urlopen(req) as resp:
return resp.read().decode()
@app.entrypoint
async def invoke(payload):
"""エントリポイント:保護 API を叩いて結果を返す"""
result = await call_protected_api(access_token="")
return {"api_response": json.loads(result)}
if __name__ == "__main__":
app.run()次にこのAgentを実行する run_local.py を実行します。
import asyncio
from agentcore_m2m import call_protected_api
async def main():
# access_token="" で呼ぶ → AgentCore が本物のトークンを注入
result = await call_protected_api(access_token="")
print("=== 保護 API からの応答 ===")
print(result)
if __name__ == "__main__":
asyncio.run(main())Created a workload identity
Created an user id
=== 保護 API からの応答 ===
{"message": "Hello from the protected API!", "authorized_client": "5od8etreqjlk8qpeq0bnpqbk3b", "scope": "my-api/read", "server_time": 1787461820}ログの Created a workload identity / Created an user id が成功しています。SDK がローカルテスト用にワークロード ID とユーザー ID を自動生成し、AgentCore Identity 経由でトークンを取得しました。そして保護 API から Hello from the protected API! が 200 で返ってきています。このコードにトークンも Client Secret も一切書いていない状態でcall_protected_api(access_token="") と空で呼んだにもかかわらず、本物のトークンを取得し置き換えています。手で curl していた資格情報がコードに含まれていなくても動的に取得して実行されています。
少しややこしいので以下で整理してみます。
curl 手動版 | AgentCore 版 | |
|---|---|---|
トークン取得 | 自分で |
|
Client Secret | コマンド/コードに登場 | Secrets Manager に隔離( |
トークンの扱い | 変数に入れて手で運ぶ | 引数に自動注入、コードに現れない |
キャッシュ/更新 | 都度取り直し(60分で失効) | AgentCore が自動管理 |
LLM への露出 | — | トークンがコンテキストに乗らない |
コードの流れは以下になっています。
@requires_access_tokenが「cognito-m2m-providerでトークンをくれ」と AgentCore Identity に依頼- AgentCore Identity が、そのプロバイダーに紐づく Secret をSecrets Manager から取り出す
- その Client Secret を使って Cognito の
/oauth2/tokenに client credentials で問い合わせ - 得たトークンを、関数の
access_token引数に注入 - 認証付きAPIにアクセス

