|
| 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. |
0 commit comments