Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "14-token-exchange-at-request-interceptor"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"bedrock-models>=0.1.58",
"requests>=2.32.5",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.backup
*.tfplan
.build/
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# AgentCore Gateway with Token Exchange at Request Interceptor - Terraform

This Terraform configuration provisions the same infrastructure as the companion Jupyter notebook (`token-exchange-at-request-interceptor.ipynb`), enabling secure token exchange and identity propagation in multi-hop agent workflows using AgentCore Gateway.

## Architecture

1. **Client** initiates requests with Cognito OAuth2 tokens (client credentials flow)
2. **AgentCore Gateway** routes requests through an interceptor for token exchange
3. **Gateway Interceptor Lambda** validates the inbound token and exchanges it for a scoped downstream token via Cognito
4. **API Gateway (OpenAPI Target)** receives the processed request with the exchanged token
5. **Strands Agent** (not provisioned by Terraform) can connect to the gateway via streamable HTTP transport

## Prerequisites

- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.0
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) configured with credentials
- AWS provider >= 5.0 (with `aws_bedrockagentcore_*` resource support)

## File Structure

```
terraform/
├── providers.tf # AWS, archive, null, random providers
├── variables.tf # region, name_prefix
├── data.tf # Account ID, region, random suffix for unique names
├── cognito.tf # User Pool, Domain, Resource Server, App Client
├── lambda.tf # Pre Token Generation + Gateway Interceptor Lambdas
├── apigateway.tf # REST API (OpenAPI), Authorizer, API Key, Usage Plan
├── agentcore.tf # Credential Provider, Gateway IAM Role, Gateway, Target
├── outputs.tf # Key outputs (IDs, ARNs, URLs)
└── lambda_src/
├── pre_token_generation/
│ └── lambda_function.py
└── gateway_interceptor/
└── lambda_function.py
```

## Resources Created

| Resource | Terraform Resource |
|---|---|
| Cognito User Pool (Essentials tier) | `aws_cognito_user_pool.this` + `null_resource.configure_user_pool` |
| Cognito Resource Server (read/write scopes) | `aws_cognito_resource_server.this` |
| Cognito App Client (client_credentials) | `aws_cognito_user_pool_client.this` |
| Cognito User Pool Domain | `aws_cognito_user_pool_domain.this` |
| Pre Token Generation Lambda + IAM Role | `aws_lambda_function.pre_token_generation` |
| Gateway Interceptor Lambda + IAM Role | `aws_lambda_function.gateway_interceptor` |
| API Gateway REST API (OpenAPI import) | `aws_api_gateway_rest_api.this` |
| Cognito Authorizer | `aws_api_gateway_authorizer.cognito` |
| API Key + Usage Plan | `aws_api_gateway_api_key.this` + `aws_api_gateway_usage_plan.this` |
| AgentCore API Key Credential Provider | `aws_bedrockagentcore_api_key_credential_provider.this` |
| AgentCore Gateway (Custom JWT + Interceptor) | `aws_bedrockagentcore_gateway.this` |
| AgentCore Gateway Target (OpenAPI) | `aws_bedrockagentcore_gateway_target.this` |

## Usage

```bash
cd terraform
terraform init
terraform apply
```

To customize the deployment:

```bash
terraform apply -var="region=us-west-2" -var="name_prefix=myproject"
```

## Variables

| Name | Description | Default |
|---|---|---|
| `region` | AWS region | `us-east-1` |
| `name_prefix` | Prefix for resource names | `agentcore` |

## Outputs

| Name | Description |
|---|---|
| `cognito_user_pool_id` | Cognito User Pool ID |
| `cognito_client_id` | Cognito App Client ID |
| `cognito_client_secret` | Cognito App Client Secret (sensitive) |
| `cognito_token_endpoint` | Cognito OAuth2 token endpoint |
| `api_gateway_url` | API Gateway invoke URL |
| `gateway_id` | AgentCore Gateway ID |
| `gateway_url` | AgentCore Gateway URL |
| `gateway_target_id` | AgentCore Gateway Target ID |

## Testing with Strands Agent

After deploying, use the outputs to connect a Strands agent:

```python
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp.mcp_client import MCPClient
from mcp.client.streamable_http import streamablehttp_client

# Use terraform output values
gateway_url = "<gateway_url from terraform output>"
access_token = "<obtain via cognito token endpoint>"

client = MCPClient(lambda: streamablehttp_client(
gateway_url,
headers={"Authorization": f"Bearer {access_token}"}
))

model = BedrockModel(model_id="us.amazon.nova-pro-v1:0")

with client:
tools = client.list_tools_sync()
agent = Agent(model=model, tools=tools)
response = agent("List all tools available to you")
```

## Cleanup

```bash
terraform destroy
```

## Design Notes

- A `random_id` suffix is used instead of timestamps to avoid resource recreation on every plan/apply.
- A `null_resource` with AWS CLI is used to upgrade the Cognito User Pool to Essentials tier and attach the V3_0 Pre Token Generation trigger, since the Terraform AWS provider does not natively support `UserPoolTier`.
- Native `aws_bedrockagentcore_*` Terraform resources are used for the gateway, target, and credential provider.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# =============================================================================
# API Key Credential Provider (required by OpenAPI targets; actual auth is
# handled by the interceptor which injects the Cognito JWT)
# =============================================================================
resource "aws_bedrockagentcore_api_key_credential_provider" "this" {
name = "api-key-provider-${local.suffix}"
api_key = "placeholder-not-used-for-auth"
}

# =============================================================================
# IAM Role for AgentCore Gateway
# =============================================================================
resource "aws_iam_role" "gateway" {
name = "AgentCoreGatewayRole-${local.suffix}"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "bedrock-agentcore.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}

resource "aws_iam_role_policy_attachment" "gateway_agentcore" {
role = aws_iam_role.gateway.name
policy_arn = "arn:aws:iam::aws:policy/BedrockAgentCoreFullAccess"
}

resource "aws_iam_role_policy" "gateway_lambda_invoke" {
name = "LambdaInvokePolicy"
role = aws_iam_role.gateway.name

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["lambda:InvokeAsync", "lambda:InvokeFunction"]
Resource = "*"
}]
})
}

# =============================================================================
# AgentCore Gateway
# =============================================================================
resource "aws_bedrockagentcore_gateway" "this" {
name = "${var.name_prefix}-gateway-${local.suffix}"
description = "AgentCore Gateway with Cognito 2LO auth - ${local.suffix}"
role_arn = aws_iam_role.gateway.arn

authorizer_type = "CUSTOM_JWT"
authorizer_configuration {
custom_jwt_authorizer {
discovery_url = "https://cognito-idp.${local.region}.amazonaws.com/${aws_cognito_user_pool.this.id}/.well-known/openid-configuration"
allowed_clients = [aws_cognito_user_pool_client.gateway.id]
allowed_scopes = [
"${local.resource_server_id}/read",
"${local.resource_server_id}/write",
]
}
}

protocol_type = "MCP"
protocol_configuration {
mcp {
supported_versions = ["2025-03-26", "2025-06-18"]
}
}

interceptor_configuration {
interception_points = ["REQUEST"]

interceptor {
lambda {
arn = aws_lambda_function.gateway_interceptor.arn
}
}

input_configuration {
pass_request_headers = true
}
}

depends_on = [
aws_iam_role_policy_attachment.gateway_agentcore,
aws_iam_role_policy.gateway_lambda_invoke,
]
}

# =============================================================================
# AgentCore Gateway Target (OpenAPI — clean spec, no API Gateway extensions)
# =============================================================================
resource "aws_bedrockagentcore_gateway_target" "this" {
name = "posts-api-target-${local.suffix}"
gateway_identifier = aws_bedrockagentcore_gateway.this.gateway_id

credential_provider_configuration {
api_key {
provider_arn = aws_bedrockagentcore_api_key_credential_provider.this.credential_provider_arn
credential_parameter_name = "X-Api-Key"
credential_location = "HEADER"
}
}

target_configuration {
mcp {
open_api_schema {
inline_payload {
payload = jsonencode(local.target_openapi_spec)
}
}
}
}
}

# Clean OpenAPI spec for the AgentCore target — no x-amazon-apigateway-*
# extensions, just standard OpenAPI that AgentCore converts into MCP tools.
locals {
target_openapi_spec = {
openapi = "3.0.1"
info = {
title = "Posts API"
version = "1.0.0"
description = "Create and manage posts"
}
servers = [{
url = local.api_gateway_url
description = "Posts API Gateway endpoint"
}]
components = {
schemas = local.schemas
}
paths = {
"/posts" = {
post = {
summary = "Create a new post"
operationId = "createPost"
requestBody = local.create_post_request_body
responses = local.create_post_responses
}
}
}
}
}
Loading
Loading