Hands-on, Terraform-deployable demos of authenticating and authorizing a real MCP client (Claude) to Amazon Bedrock AgentCore Gateway, with Okta as the IdP — exploring two different downstream-auth models, plus the least-privilege provisioning pattern that stands them up.
Everything is verified working end to end (a user logs in via Okta from Claude and per-tool authorization is enforced), and documented with the AWS-isms / Okta-isms / lessons learned we hit along the way.
| Folder / file | Pattern | In one line |
|---|---|---|
pattern_token_exchange/ |
On-behalf-of (RFC 8693) | The gateway exchanges the user's token for a new, per-audience downstream token that carries the user; per-tool authz via a gateway interceptor. Needs a small CLI shim (the TF provider can't express OBO yet). |
pattern_service_account/ |
Service account + scopes | No token exchange: one token (aud + scopes for the API), gateway reaches the Lambda over SigV4 as a service account and forwards the same claims. Scopes decide read/write. 100% native Terraform. Security-data-lake theme. |
bootstrap/ |
Least-priv provisioning broker | The one-time privileged Okta setup (API Services app, Private Key JWT, scoped *.manage only) that a repeatable low-priv run uses instead of a standing admin token. Local. |
pattern_account_linking_okta/ |
Account linking / user-consent 3LO (USER_FEDERATION) | Okta SSO (a real, verified login) proves who's asking; the agent then mints a GitHub consent URL, the user approves once, and AgentCore's token vault holds the delegated GitHub token — a join table linking two otherwise-unrelated identities, not claim-based OBO. CLI or a serverless web UI. |
pattern_agent_token_exchange/ |
On-behalf-of (RFC 8693), agent-side | The agent process itself — no gateway in front — exchanges the user's inbound Okta access token for a different-audience downstream Okta token that still carries the same sub: true claim-based identity propagation, minted by the agent instead of a gateway. Never leaves Okta. |
production-architecture.html |
Topology | How this looks in the real world: one MCP front door, privilege distributed across per-system scoped executors. Concentrate the interface, distribute the privilege. |
Each folder is a self-contained Terraform root with its own README, a runnable
end-to-end tester (login.py or agent.py, per the pattern's README), and
terraform.tfvars.example.
- The downstream API must authorize the individual user itself, via a gateway
already in front of it (per-user data, fine-grained at the resource) →
token exchange (
pattern_token_exchange). The Lambda gets a token with the user'ssuband its own audience. - One service account fronts the API and scope is the gate (read vs write,
role-ish) → service account (
pattern_service_account). Simpler infra; the gateway is a scoped robot and the token's scopes decide what's allowed. - There's no pre-existing enterprise IdP delegation at all, and the agent needs
a user's interactive consent to an external SaaS (GitHub here, not Okta) →
account linking (
pattern_account_linking_okta). No silent exchange — a human clicks "Authorize" on the provider's own consent screen (fronted by a real, verified Okta SSO login), and AgentCore Identity's token vault holds the resulting delegated token as a join-table entry, not a claim. - The user's identity must reach your own downstream API directly from the
agent — no gateway in front, no third-party consent screen, just the agent
minting a downstream credential for itself → agent-side token exchange
(
pattern_agent_token_exchange). The agent calls AgentCore Identity directly and swaps the inbound Okta token for a different-audience Okta token carrying the samesub— true claim-based OBO, never leaving Okta.
The first two share the same front end (Claude → gateway → Okta login → gateway interceptor for per-tool authz); they differ only in how the downstream call is authorized.
Pick a folder and follow its README. Minimum you need everywhere:
- An IdP: a free Okta Integrator/developer org + an API token for
pattern_token_exchange/pattern_service_account/pattern_account_linking_okta/pattern_agent_token_exchange— plus a GitHub App forpattern_account_linking_okta's outbound consent leg (its inbound leg is Okta; GitHub is only the third-party being linked to). - AWS creds for
us-east-1/us-west-2, current aws-cli v2,jq. - Claude Code ≥ 2.1.30 to connect as an MCP client (static-client OAuth flags)
— only relevant to the two gateway-fronted patterns
(
pattern_token_exchange/pattern_service_account);pattern_account_linking_oktaandpattern_agent_token_exchangeare CLI-driven demos that don't put Claude in the loop at all.
cd pattern_service_account # or pattern_token_exchange
cp terraform.tfvars.example terraform.tfvars # fill in Okta + demo_user_login
terraform init && terraform apply
python3 login.py # log in via Okta and exercise the toolspattern_account_linking_okta and pattern_agent_token_exchange follow the same
shape (terraform.tfvars.example → terraform apply) but each needs its own
credentials up front and has its own phased quickstart in its README:
pattern_account_linking_okta needs both an Okta org and a GitHub App (fill
in tfvars, apply, then paste the AgentCore-minted callback URL into the GitHub
App by hand); pattern_agent_token_exchange needs only an Okta org — no
GitHub, no third party of any kind.
- Lambdas decode JWTs without verifying signatures; some targets are reachable directly — fine for tracing the flow, not production. Each README lists the hardening (JWKS verification, lock down URLs, scope IAM).
- Secrets live only in
terraform.tfvars+ state, both gitignored. Provider lock files are committed for reproducibility.
Built iteratively with Claude Code — including the debugging journey documented in each pattern's README (the parts that weren't in any single doc).