Skip to content

Commit 72bed16

Browse files
committed
feat: identity propagation, Cedar policy, code cleanup and documentation updates
1 parent 9b833b1 commit 72bed16

27 files changed

Lines changed: 602 additions & 188 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ What comes next? That's up to you, the developer. With your requirements in mind
5555
The out-of-the-box architecture is shown above. The diagram illustrates the authentication flows across the stack:
5656
1. User login to the frontend (Cognito User Pool — Authorization Code grant): The user authenticates with Cognito via the web application hosted on AWS Amplify. Cognito issues a JWT access token for the session.
5757
2. Frontend to AgentCore Runtime (Cognito User Pool JWT validation): The frontend passes the user's JWT in the Authorization header. The Runtime validates the token against the Cognito User Pool.
58-
3. AgentCore Runtime to AgentCore Gateway (OAuth2 Client Credentials / M2M): The Runtime authenticates as a service using the OAuth2 Client Credentials grant — independent of the user's identity. AgentCore Identity manages token retrieval via the Token Vault.
58+
3. AgentCore Runtime to AgentCore Gateway (OAuth2 Client Credentials / M2M): The Runtime authenticates using the OAuth2 Client Credentials grant with user identity propagated into the M2M token via the Cognito V3 Pre-Token Lambda. The Gateway evaluates Cedar policies against the user's claims to enforce fine-grained access control.
5959
4. Frontend to API Gateway (Cognito User Pool JWT validation): API requests are authenticated using a Cognito User Pools Authorizer with the same user JWT from Flow 1.
6060

6161
### Tech Stack
@@ -100,7 +100,9 @@ fullstack-agentcore-solution-template/
100100
│ │ └── fast-main-stack.ts
101101
│ ├── bin/ # CDK app entry point
102102
│ ├── lambdas/ # Lambda function code
103+
│ │ ├── cedar-policy/ # Cedar Policy Engine lifecycle
103104
│ │ ├── oauth2-provider/ # OAuth2 Credential Provider lifecycle
105+
│ │ ├── pretoken-v3/ # Cognito V3 Pre-Token Generation Lambda
104106
│ │ ├── feedback/ # Feedback API handler
105107
│ │ └── zip-packager/ # Runtime ZIP packager
106108
│ └── config.yaml # Deployment configuration
@@ -130,6 +132,8 @@ fullstack-agentcore-solution-template/
130132
│ └── code_interpreter/ # AgentCore Code Interpreter integration
131133
│ └── code_interpreter_tools.py # Core implementation
132134
├── gateway/ # Gateway utilities and tools
135+
│ ├── policies/ # Cedar policy definitions
136+
│ │ └── policy.cedar # Department-based access control policy
133137
│ └── tools/ # Gateway tool implementations
134138
│ └── sample_tool/ # Example Gateway tool
135139
├── scripts/ # Deployment and utility scripts
@@ -151,6 +155,7 @@ fullstack-agentcore-solution-template/
151155
│ ├── AGENT_CONFIGURATION.md # Agent setup guide
152156
│ ├── MEMORY_INTEGRATION.md # Memory integration guide
153157
│ ├── GATEWAY.md # Gateway integration guide
158+
│ ├── IDENTITY_POLICY.md # Identity propagation & Cedar policy guide
154159
│ ├── RUNTIME_GATEWAY_AUTH.md # M2M authentication workflow
155160
│ ├── STREAMING.md # Streaming implementation guide
156161
│ ├── TOOL_AC_CODE_INTERPRETER.md # Code Interpreter guide

docs/AGENT_CONFIGURATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ bedrock_model = ChatBedrock(
7575
**Gateway Integration** (`patterns/langgraph-single-agent/langgraph_agent.py`):
7676

7777
```python
78-
# Create MCP client for Gateway
79-
mcp_client = await create_gateway_mcp_client(access_token)
78+
# Create MCP client for Gateway with user identity propagation
79+
mcp_client = await create_gateway_mcp_client(user_id)
8080

8181
# Load tools from Gateway
8282
tools = await mcp_client.get_tools()

docs/DEPLOYMENT.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ All interface endpoints must have private DNS enabled and must be associated wit
131131

132132
#### NAT Gateway
133133

134-
A NAT Gateway is **not required** for VPC mode. The agent authenticates with the AgentCore Gateway using the Token Vault OAuth2 Credential Provider, which retrieves tokens via the AgentCore Identity API. This API is reachable through the `bedrock-agent-runtime` VPC endpoint, so no outbound internet access is needed. All AWS service traffic (Bedrock, SSM, Secrets Manager, etc.) stays internal via VPC endpoints.
134+
A NAT Gateway is **required** if you use identity-aware Gateway authentication (Approach 1 in `patterns/*/tools/gateway.py`). This approach calls the Cognito `/oauth2/token` hosted domain endpoint directly to propagate user identity into M2M tokens. The Cognito hosted domain is a public HTTPS endpoint with no VPC endpoint available, so outbound internet access via NAT Gateway is needed.
135135

136-
> **Note:** If you add custom tools or integrations that make outbound internet calls, you will need a NAT Gateway in a public subnet with a `0.0.0.0/0` route from your private subnets.
136+
If you use the standard `@requires_access_token` decorator (Approach 2), a NAT Gateway is **not required** — the AgentCore Identity service handles the Cognito token exchange server-side within AWS, reachable through the `bedrock-agentcore` VPC endpoint.
137+
138+
> **Note:** If you add custom tools or integrations that make outbound internet calls, you will also need a NAT Gateway in a public subnet with a `0.0.0.0/0` route from your private subnets.
137139

138140
#### Security Group Configuration
139141

docs/GATEWAY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ const gateway = new bedrockagentcore.CfnGateway(this, "AgentCoreGateway", {
444444

445445
## Related Documentation
446446

447+
- [Identity Propagation & Cedar Policy Guide](IDENTITY_POLICY.md) - User-level access control for Gateway tools
448+
- [Runtime-Gateway Authentication](RUNTIME_GATEWAY_AUTH.md) - M2M token flow between Runtime and Gateway
447449
- [Deployment Guide](DEPLOYMENT.md) - How to deploy FAST infrastructure
448450
- [AWS AgentCore Gateway Documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore-gateway.html) - Official AWS documentation
449451
- [AWS Gateway Lambda Target Documentation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway-add-target-lambda.html) - Lambda target implementation details

docs/IDENTITY_POLICY.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# Identity Propagation & Cedar Policy Guide
2+
3+
This document describes how FAST propagates user identity from the frontend through to AgentCore Gateway Cedar policies, enabling fine-grained, user-level access control on Gateway tools.
4+
5+
## Overview
6+
7+
AgentCore Gateway authenticates requests using OAuth2 tokens validated by a CUSTOM_JWT authorizer. By default, the Runtime obtains M2M tokens via the Client Credentials flow, and all requests carry the same machine identity. This means the Gateway cannot distinguish between individual users.
8+
9+
This feature adds **identity propagation** on top of the existing M2M flow: the authenticated user's identity is embedded into the M2M token using Cognito's `aws_client_metadata` parameter and V3 Pre-Token Lambda trigger. The enriched token is then evaluated by Cedar policies at the Gateway, enabling access control rules like "only users in the finance department can access the billing tool."
10+
11+
**Use this when:** Gateway tools need user-level access control based on attributes like department, role, or user ID.
12+
13+
## Architecture / Flow
14+
15+
The identity propagation flow has six steps:
16+
17+
```
18+
1. User logs in → Frontend gets JWT from Cognito
19+
2. Frontend sends request → Runtime validates JWT, extracts user_id (sub claim)
20+
3. Runtime calls Cognito /oauth2/token with aws_client_metadata containing user_id
21+
4. Cognito V3 Pre-Token Lambda fires → reads user_id → injects department/role claims into M2M token
22+
5. Runtime calls Gateway tool with the enriched M2M token
23+
6. Gateway's CUSTOM_JWT Authorizer maps token claims to Cedar principal tags → Policy Engine evaluates Cedar policy → allow or deny
24+
```
25+
26+
Key security property: the `user_id` comes from the validated JWT in the Runtime's Session Context (`sub` claim), not from the LLM or request payload. This ensures the identity chain is cryptographically secure end-to-end.
27+
28+
## Components
29+
30+
### Cognito ESSENTIALS Tier
31+
32+
**File:** `infra-cdk/lib/cognito-stack.ts`
33+
34+
The Cognito User Pool is configured with `featurePlan: ESSENTIALS`. This is required because V3 Pre-Token Generation Lambda triggers only fire on Client Credentials (M2M) grants when the ESSENTIALS tier is enabled. Without it, the Pre-Token Lambda would not be invoked during M2M token generation.
35+
36+
### V3 Pre-Token Lambda
37+
38+
**File:** `infra-cdk/lambdas/pretoken-v3/index.py`
39+
40+
This Lambda fires on every token generation event (both user login and M2M). It only processes M2M flows (`TokenGeneration_ClientCredentials`) and skips user login flows.
41+
42+
For M2M flows, it reads `verified_user_id` from `clientMetadata` and assigns department/role claims based on the user's identity:
43+
44+
| User Email Contains | Department | Role |
45+
|---------------------|------------|------|
46+
| `alice` | finance | admin |
47+
| `bob` | engineering | developer |
48+
| (anything else) | guest | viewer |
49+
50+
These claims are injected into the M2M access token via `claimsToAddOrOverride`:
51+
- `user_id` — the authenticated user's ID
52+
- `department` — the user's department
53+
- `role` — the user's role
54+
55+
To use dynamic group assignment, replace the hardcoded logic in the Pre-Token Lambda with a DynamoDB lookup, directory service query, or other identity provider.
56+
57+
### Cedar Policy File
58+
59+
**File:** `gateway/policies/policy.cedar`
60+
61+
The Cedar policy defines access control rules for Gateway tools. It is loaded by CDK at deploy time, with `//` comment lines stripped and the `{{GATEWAY_ARN}}` placeholder replaced with the actual Gateway ARN.
62+
63+
Two policy versions are provided:
64+
65+
**Version 1 (Active by default):** All departments — including guest — can access the tool.
66+
67+
```cedar
68+
permit(
69+
principal is AgentCore::OAuthUser,
70+
action == AgentCore::Action::"sample-tool-target___text_analysis_tool",
71+
resource == AgentCore::Gateway::"{{GATEWAY_ARN}}"
72+
)
73+
when {
74+
principal.hasTag("department") &&
75+
(principal.getTag("department") == "finance" ||
76+
principal.getTag("department") == "engineering" ||
77+
principal.getTag("department") == "guest")
78+
};
79+
```
80+
81+
**Version 2 (Commented out):** Only finance and engineering can access the tool. Guests are denied automatically because Cedar is deny-by-default.
82+
83+
```cedar
84+
permit(
85+
principal is AgentCore::OAuthUser,
86+
action == AgentCore::Action::"sample-tool-target___text_analysis_tool",
87+
resource == AgentCore::Gateway::"{{GATEWAY_ARN}}"
88+
)
89+
when {
90+
principal.hasTag("department") &&
91+
(principal.getTag("department") == "finance" ||
92+
principal.getTag("department") == "engineering")
93+
};
94+
```
95+
96+
To switch versions: edit `gateway/policies/policy.cedar` (comment out one version, uncomment the other), then run `cdk deploy`.
97+
98+
### Policy Engine Custom Resource
99+
100+
**Files:**
101+
- `infra-cdk/lambdas/cedar-policy/index.py` — Custom Resource Lambda
102+
- `infra-cdk/lib/backend-stack.ts` — CDK resource definition
103+
104+
A CloudFormation Custom Resource manages the full Policy Engine lifecycle because no L1/L2 CDK construct exists for AgentCore Policy. The Lambda handles three CloudFormation events:
105+
106+
- **Create:** Creates Policy Engine → creates Cedar Policy → attaches Policy Engine to Gateway
107+
- **Update:** Deletes existing policies → creates new policy with updated document → verifies engine is still attached to Gateway
108+
- **Delete:** Detaches Policy Engine from Gateway → deletes all policies → deletes Policy Engine
109+
110+
All operations use official boto3 waiters (`policy_engine_active`, `policy_engine_deleted`, `policy_active`, `policy_deleted`). Gateway status changes use a custom polling loop as no official waiter exists.
111+
112+
### Gateway Authorizer
113+
114+
**File:** `infra-cdk/lib/backend-stack.ts`
115+
116+
The Gateway uses a `CUSTOM_JWT` authorizer configured with the Cognito OIDC discovery URL and the machine client ID. The authorizer validates M2M tokens and automatically maps all JWT claims to Cedar principal tags:
117+
118+
| JWT Claim | Cedar Principal Tag |
119+
|-----------|-------------------|
120+
| `department` | `principal.getTag("department")` |
121+
| `role` | `principal.getTag("role")` |
122+
| `user_id` | `principal.getTag("user_id")` |
123+
124+
## Cedar Policy Guide
125+
126+
### Policy File Location
127+
128+
`gateway/policies/policy.cedar` — edit this file and run `cdk deploy` to apply changes. The Custom Resource Lambda detects the change and updates the policy in-place without recreating the Policy Engine.
129+
130+
### Action Name Format
131+
132+
Cedar action names follow the format: `<TargetName>___<tool_name>` (triple underscore).
133+
134+
- **TargetName** comes from the `CfnGatewayTarget` name in `backend-stack.ts` (e.g., `sample-tool-target`)
135+
- **tool_name** comes from `tool_spec.json` (e.g., `text_analysis_tool`)
136+
- Combined: `sample-tool-target___text_analysis_tool`
137+
138+
These are case-sensitive. A mismatch silently denies all requests even when the policy logic looks correct.
139+
140+
### Deny-by-Default
141+
142+
Cedar is deny-by-default: if no `permit` statement matches a request, it is automatically denied. An explicit `forbid` statement is not needed to block access — simply omit the department from the permit's conditions.
143+
144+
For example, to deny guests, remove `"guest"` from the department list. No `forbid` statement is required.
145+
146+
### Adding New Tools
147+
148+
When adding a new Gateway target and tool:
149+
150+
1. Create the new Lambda tool and `CfnGatewayTarget` in `backend-stack.ts`
151+
2. Add a new `permit` statement to `policy.cedar` with the correct action name
152+
3. Run `cdk deploy`
153+
154+
Each `create_policy` call creates one policy containing one Cedar statement. The Custom Resource currently creates a single policy per deploy. To add multiple policies (e.g., separate permit and forbid statements), update the Custom Resource Lambda to call `create_policy()` once per statement.
155+
156+
## Two Authentication Approaches
157+
158+
FAST provides two approaches for Gateway authentication in each pattern's `tools/gateway.py`:
159+
160+
### Approach 1 (Active): Direct Cognito Call
161+
162+
Calls the Cognito `/oauth2/token` endpoint directly with `aws_client_metadata` containing the user's identity. The V3 Pre-Token Lambda reads this metadata and injects user-specific claims into the M2M token.
163+
164+
**Use when:** The M2M token needs to carry user-specific claims for Cedar policy evaluation.
165+
166+
**Trade-off:** Requires outbound HTTPS access to the Cognito hosted domain (NAT Gateway needed in VPC mode).
167+
168+
### Approach 2 (Commented Out): @requires_access_token Decorator
169+
170+
Uses the AgentCore Identity SDK decorator for automatic token retrieval, caching, and refresh via the Token Vault. Simpler setup, but does not support `aws_client_metadata`, so the Pre-Token Lambda cannot identify the user.
171+
172+
**Use when:** Pure M2M authentication is sufficient and no user identity is needed in the token.
173+
174+
### Switching from Approach 1 to Approach 2
175+
176+
Each pattern's `tools/gateway.py` contains both approaches with switching instructions:
177+
178+
1. Uncomment the decorator-based `_fetch_gateway_token()` function
179+
2. Comment out the Approach 1 `create_gateway_mcp_client(user_id)`
180+
3. Uncomment the Approach 2 `create_gateway_mcp_client()` (no `user_id` param)
181+
4. Update callers to not pass `user_id`
182+
5. Verify `GATEWAY_CREDENTIAL_PROVIDER_NAME` env var is set in the CDK Runtime config (already configured in `backend-stack.ts`)
183+
184+
## Customization
185+
186+
### Changing Group Assignment
187+
188+
Edit `infra-cdk/lambdas/pretoken-v3/index.py` to replace the hardcoded email-based logic with your own identity resolution. For example:
189+
190+
- Query a DynamoDB table mapping user IDs to departments
191+
- Call an external directory service (LDAP, Active Directory)
192+
- Call the Cognito API (`AdminGetUser`) to read user attributes or group membership for the `user_id` received in `clientMetadata`
193+
194+
### Adding New Claims
195+
196+
To add new claims to the M2M token:
197+
198+
1. Add the claim to `claimsToAddOrOverride` in the Pre-Token Lambda
199+
2. Reference the claim in Cedar policy using `principal.getTag("claim_name")`
200+
3. No Gateway configuration change is needed — the CUSTOM_JWT authorizer maps all JWT claims to Cedar tags automatically
201+
202+
### VPC Mode
203+
204+
When deploying in VPC mode, Approach 1 (direct Cognito call) requires a **NAT Gateway** because the Cognito `/oauth2/token` hosted domain is a public HTTPS endpoint with no VPC endpoint available.
205+
206+
Approach 2 (`@requires_access_token` decorator) does not require a NAT Gateway — the AgentCore Identity service handles the Cognito token exchange server-side within AWS, reachable through the `bedrock-agentcore` VPC endpoint.
207+
208+
See `docs/DEPLOYMENT.md` for full VPC configuration details.

docs/RUNTIME_GATEWAY_AUTH.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ The stack contains two distinct authentication flows that are independent of eac
4848
- Uses the Client Credentials grant (no human user involved)
4949
- The M2M token is obtained by the Runtime via AgentCore Identity's Token Vault and used to authenticate calls to the Gateway
5050
- No user identity is involved in this flow at any point
51+
- To propagate user identity into M2M tokens for Cedar policy evaluation, see [Identity Propagation & Cedar Policy Guide](IDENTITY_POLICY.md)
5152

5253
The two flows are parallel, not nested:
5354

gateway/policies/policy.cedar

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
// - Cedar is deny-by-default: if no permit statement matches a request, it is
1414
// automatically denied. An explicit forbid statement is not needed to block
1515
// access — simply omit the department from the permit's OR conditions.
16-
// - The AgentCore create_policy API accepts ONE Cedar statement per policy.
17-
// To define multiple access rules, use the || (OR) operator within a single
18-
// permit statement rather than writing multiple permit statements.
16+
// - Each create_policy call creates one policy containing one Cedar statement.
17+
// You can call create_policy multiple times to add multiple policies to the
18+
// same engine. Alternatively, use || or action in [...] to combine rules
19+
// within a single statement.
1920
// - The Cedar action name format is: "<TargetName>___<tool_name>" (triple underscore).
2021
// Tool name comes from tool_spec.json, target name from the CfnGatewayTarget.
2122
//
@@ -27,38 +28,23 @@
2728
// the Policy Engine.
2829
//
2930
// ========================================
30-
// VERSION 0: Permissive Wildcard (Action Name Discovery)
31+
// VERSION 1: Guest Has Full Access (Testing "Allow" Case)
3132
// ========================================
32-
// Deploy this version first when setting up a new gateway target to discover
33-
// the exact action name from CloudWatch logs, then switch to a scoped version.
33+
// All departments (finance, engineering, guest) can access the tool.
3434

3535
permit(
3636
principal is AgentCore::OAuthUser,
37-
action,
37+
action == AgentCore::Action::"sample-tool-target___text_analysis_tool",
3838
resource == AgentCore::Gateway::"{{GATEWAY_ARN}}"
3939
)
4040
when {
41-
principal.hasTag("department")
41+
principal.hasTag("department") &&
42+
(principal.getTag("department") == "finance" ||
43+
principal.getTag("department") == "engineering" ||
44+
principal.getTag("department") == "guest")
4245
};
4346
//
4447
// ========================================
45-
// VERSION 1: Guest Has Full Access (Testing "Allow" Case)
46-
// ========================================
47-
// All departments (finance, engineering, guest) can access the tool.
48-
//
49-
// permit(
50-
// principal is AgentCore::OAuthUser,
51-
// action == AgentCore::Action::"sample-tool-target___text_analysis_tool",
52-
// resource == AgentCore::Gateway::"{{GATEWAY_ARN}}"
53-
// )
54-
// when {
55-
// principal.hasTag("department") &&
56-
// (principal.getTag("department") == "finance" ||
57-
// principal.getTag("department") == "engineering" ||
58-
// principal.getTag("department") == "guest")
59-
// };
60-
//
61-
// ========================================
6248
// VERSION 2: Guest Denied (Testing "Deny" Case)
6349
// ========================================
6450
// Only finance and engineering can access the tool. Guests are denied

0 commit comments

Comments
 (0)