MCP OAuth on AgentCore Gateway + Cognito via APIGW Façade
Overview
Introduction
Amazon Bedrock AgentCore Gateway is the most pragmatic way to host a Model Context Protocol server on AWS today. Declare your tools as OpenAPI or as Lambda targets, get a managed multi-target MCP endpoint, and inherit AWS-native authentication via a customJwtAuthorizer. For machine-to-machine traffic that pattern is excellent.
The moment you ask an interactive MCP client — Claude Code, Cursor, the MCP Inspector — to talk to that same gateway with a per-user OAuth flow, the seams show. AgentCore Gateway expects a JWT and trusts whatever issuer you wired into its authorizer. Pair it with Amazon Cognito and the wiring works for the server side. It does not work for the client side, because Cognito is an OIDC identity provider, not an MCP-compliant authorization server. The two are not the same thing.
The MCP authorization spec is built on a specific stack of IETF RFCs — RFC 9728 (Protected Resource Metadata), RFC 8414 (Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), and RFC 7636 (PKCE). I covered the full stack in my MCP authorization deep-dive and showed how Keycloak fills it cleanly with one workaround. Cognito does not fill it. Without those RFCs, Claude Code never gets past metadata discovery and reports Failed to connect.
This post walks through an architecture I shipped recently: a small API Gateway + Lambda façade that adds the missing RFC surfaces in front of an AgentCore Gateway backed by Cognito. The result is a claude mcp add <https-url> that just works, while keeping Cognito as the identity provider and AgentCore Gateway as the multi-target MCP runtime.
Why AgentCore Gateway
If you treat AgentCore Gateway purely as an MCP transport, the value is real and worth restating before we critique the auth story.
- Multi-target multiplexing. One MCP endpoint fronts many backends — OpenAPI HTTP services, Lambda functions, even Smithy models — without your client ever knowing they're separate.
- Two backend shapes that cover most needs. External HTTP backends (with their own JWT authorizer) plug in via OpenAPI; in-account Lambda backends invoked via the gateway's IAM role need no second auth layer at all.
- Tool-level interceptors. A gateway-attached Lambda can run on every
tools/listandtools/callto gate which tools a given JWT scope sees. The interceptor is your scope-to-tool mapping in code. - In-place updates of the JWT authorizer's
allowedClients. Adding or removing a tenant's M2M client does not rotate the gateway URL. That is the kind of operational property you only appreciate after living without it. - Managed scaling, observability, and session isolation. No fleet to run, no transport code to maintain.
For a sense of how an OAuth2 client interacts with this surface from the client side, see my earlier walkthroughs on invoking AgentCore-hosted MCP servers and using the MCP SDK's OAuthClientProvider.
Where Cognito Falls Short for MCP
AgentCore Gateway's customJwtAuthorizer validates inbound JWTs against a discoveryUrl and an allowedClients list. Point that at Cognito, M2M traffic with client_credentials works immediately. Interactive flows are where the spec asymmetry bites.
The MCP authorization spec asks the resource to advertise its authorization server, and asks the authorization server to support a small handful of behaviours that Cognito does not.
| RFC | What MCP requires | What Cognito does |
|---|---|---|
| RFC 9728 — Protected Resource Metadata | /.well-known/oauth-protected-resource returns the resource identifier and links to its authorization_servers | Not served by Cognito; AgentCore Gateway emits the header but points at the OIDC issuer |
| RFC 8414 — Authorization Server Metadata | /.well-known/oauth-authorization-server under the issuer URL | Cognito only serves OIDC discovery at /.well-known/openid-configuration under the cognito-idp.<region>.amazonaws.com/<pool-id> path |
| RFC 7591 — Dynamic Client Registration | Public registration_endpoint so clients with no preconfigured client_id can self-register | No public DCR endpoint; admin-only CreateUserPoolClient API |
RFC 6749 §3.1.2.3 — exact redirect_uri match | Native MCP clients open OAuth callback listeners on a random ephemeral port | Hosted UI requires the redirect_uri to match a pre-registered callbackUrls entry exactly — you cannot enumerate every port |
Each gap is independently a hard stop for Claude Code. Together they explain a behaviour I watched many times during the spike: paste the gateway URL into claude mcp add, see Claude Code attempt RFC 9728 metadata discovery, hit an issuer that does not serve RFC 8414, and surface a generic "Failed to connect."
The third gap — DCR — is structural. MCP's design assumes a federated ecosystem where clients are not pre-provisioned by every server they want to talk to. SEP-991 softens that with Client ID Metadata Documents, but Claude Code's stable build still expects a registration endpoint to be advertised, and Cognito does not have one. The fourth gap is the most operationally annoying: Cognito's exact-match callback URL rule (per RFC 6749 §3.1.2.3) is correct from a security perspective but incompatible with native-app loopback redirects (RFC 8252 §7.3) on random ports.
This is the same shape of friction I documented for Keycloak — see Implementing MCP OAuth 2.1 with Keycloak on AWS — except Keycloak only needed the RFC 8707 audience workaround and was otherwise compliant out of the box. Cognito needs four gaps closed, not one.
The Façade Pattern
Closing those four gaps does not require replacing Cognito or AgentCore. It requires putting a thin, stateless adapter in front of both, which:
- Serves RFC 9728 / RFC 8414 metadata that points back at itself for
authorization_endpoint,token_endpoint, andregistration_endpoint— so the client never tries to GET RFC 8414 from a Cognito URL where it does not exist. - Implements RFC 7591 by returning the same pre-provisioned Cognito user-pool app client to every caller. Conceptually a "fake" DCR, behaviourally indistinguishable from the real thing for a confidential client whose secret is held by the façade.
- Acts as the single registered Cognito callback URL and 302-redirects the authorization code to whatever loopback port the client picked. State is round-tripped through Cognito as an HMAC-signed opaque blob so the proxy stays stateless.
- Proxies everything else straight to AgentCore Gateway, including the
/mcpendpoint that carries the actual JSON-RPC traffic.
That is one Lambda behind one HTTP API in front of one AgentCore Gateway. The full architecture:
flowchart LR
Client[Claude Code MCP Client] -->|"1. GET /.well-known/oauth-protected-resource"| Facade
Client -->|"2. GET /.well-known/oauth-authorization-server"| Facade
Client -->|"3. POST /register (RFC 7591)"| Facade
Client -->|"4. GET /oauth/authorize"| Facade
Facade -->|"5. 302 to Cognito Hosted UI"| Cognito
Cognito -->|"6. user signs in via Feishu / Cognito"| Cognito
Cognito -->|"7. callback to facade"| Facade
Facade -->|"8. 302 to localhost ephemeral port"| Client
Client -->|"9. POST /oauth/token"| Facade
Facade -->|"10. forward to Cognito"| Cognito
Client -->|"11. POST /mcp with Bearer JWT"| Facade
Facade -->|"12. proxy"| AgentCore[AgentCore Gateway]
AgentCore -->|"13. JWT validated"| AgentCore
AgentCore -->|"14. invoke target"| Backend[OpenAPI / Lambda Targets]
The façade does four small jobs. The agent runtime does the heavy lifting. Cognito remains the source of truth for identity. Nothing in the AgentCore Gateway configuration changes — its customJwtAuthorizer still trusts the Cognito issuer, the same as for an M2M client.
Implementation: Four Routes That Matter
The façade lives in src/lambdas/oauth2-facade/handler.ts. It has more routes than I'll show here, but four of them carry the architectural weight. I'll walk each.
Route 1: RFC 9728 Protected Resource Metadata
When AgentCore Gateway returns a 401, it emits a WWW-Authenticate: Bearer resource_metadata="…" header per RFC 9728. The MCP client follows that URL to learn which authorization server protects the resource. The façade serves the metadata itself and points back at itself as the authorization server:
1if (path === "/.well-known/oauth-protected-resource") {
2 return json(200, {
3 resource: `${base}/mcp`,
4 authorization_servers: [base],
5 scopes_supported: RESOURCE_SCOPES,
6 bearer_methods_supported: ["header"],
7 });
8}
The crucial line is authorization_servers: [base]. Pointing at Cognito's OIDC issuer here would push the client straight back into the RFC 8414 gap. Pointing at the façade keeps discovery on a path the façade controls.
Route 2: RFC 8414 Authorization Server Metadata
The same idea, one step further. The façade advertises itself as the authorization_endpoint and token_endpoint, while passing through userinfo, revocation, and jwks_uri to Cognito because those endpoints do not need any redirect proxying:
1if (path === "/.well-known/oauth-authorization-server") {
2 return json(200, {
3 issuer: ISSUER,
4 authorization_endpoint: `${base}/oauth/authorize`,
5 token_endpoint: `${base}/oauth/token`,
6 userinfo_endpoint: USERINFO,
7 revocation_endpoint: REVOCATION,
8 jwks_uri: JWKS,
9 registration_endpoint: `${base}/register`,
10 response_types_supported: ["code"],
11 grant_types_supported: ["authorization_code", "refresh_token"],
12 code_challenge_methods_supported: ["S256"],
13 token_endpoint_auth_methods_supported: [
14 "client_secret_basic",
15 "client_secret_post",
16 ],
17 scopes_supported: ["openid", "email", "profile", ...RESOURCE_SCOPES],
18 });
19}
issuer stays as the real Cognito issuer because the JWT's iss claim will carry that value. AgentCore Gateway's customJwtAuthorizer validates iss against the discovery URL it was configured with. If the façade lied about the issuer here, the JWT would still pass through Cognito unchanged and AgentCore would reject it. The façade rewrites paths, not claims.
code_challenge_methods_supported is ["S256"] only — the proxy refuses plain PKCE explicitly, because plain transmits the verifier unhashed and negates the protection PKCE was meant to provide.
Route 3: RFC 7591 Dynamic Client Registration
Cognito has no public DCR endpoint. Building one would mean exposing privileged Cognito admin APIs. The pragmatic alternative is to acknowledge that, in this deployment shape, every MCP client ends up using the same Cognito user-flow app client — its identity is the human user, not the calling application. So the façade implements a "fake DCR" that returns the same pre-provisioned client to every caller:
1if (path === "/register" && method === "POST") {
2 let req: { redirect_uris?: string[] } = {};
3 try { req = JSON.parse(event.body ?? "{}"); } catch {}
4 return json(201, {
5 client_id: USER_CLIENT_ID,
6 client_secret: USER_CLIENT_SECRET,
7 client_id_issued_at: Math.floor(Date.now() / 1000),
8 client_secret_expires_at: 0,
9 redirect_uris: req.redirect_uris ?? ["http://localhost:8080/callback"],
10 grant_types: ["authorization_code", "refresh_token"],
11 response_types: ["code"],
12 token_endpoint_auth_method: "client_secret_post",
13 scope: ["openid", "email", ...RESOURCE_SCOPES].join(" "),
14 });
15}
This satisfies Claude Code's expectation that a registration_endpoint exists and returns a client_id it can drive an authorization-code flow with. The trade-off — every MCP client ends up sharing one Cognito app client — is acceptable because the user identity flows through the JWT, not through client_id. If you need per-tenant isolation, partition by Cognito group / scope rather than by app client. SEP-991's URL-based client identity, if and when Claude Code adopts it, removes this trade-off entirely; until then, the fake DCR is the cleanest shim.
Route 4: The Authorization-Code Redirect Proxy
This is where the work gets interesting. Cognito requires callbackUrls to match exactly per RFC 6749 §3.1.2.3; native apps per RFC 8252 §7.3 use loopback URIs with arbitrary ports. Both rules are correct. They are not jointly satisfiable without an indirection.
The proxy collapses to three handlers. GET /oauth/authorize rewrites the redirect to point at the façade and signs the original state + redirect_uri into an HMAC blob:
1// Defense against open-redirector abuse: only loopback URIs round-trip
2if (!isAllowedClientRedirect(clientRedirect)) {
3 return json(400, { error: "invalid_request",
4 error_description: "redirect_uri must be a loopback URL" });
5}
6if (!inParams.get("code_challenge")) {
7 return json(400, { error: "invalid_request",
8 error_description: "code_challenge is required (PKCE)" });
9}
10
11const facadeState = signState(
12 { cs: clientState, r: clientRedirect },
13 HMAC_KEY,
14 Date.now(),
15);
16
17const out = new URLSearchParams();
18for (const [k, v] of inParams.entries()) {
19 if (k === "redirect_uri" || k === "state") continue;
20 out.append(k, v);
21}
22out.set("redirect_uri", `${base}/oauth/callback`);
23out.set("state", facadeState);
24
25return redirect(`${AUTHORIZE}?${out.toString()}`);
GET /oauth/callback verifies the HMAC, extracts the original loopback URL, and 302s the auth code there:
1const decoded = verifyState(facadeState, HMAC_KEY, Date.now());
2if (!decoded) return json(400, { error: "invalid_state", … });
3
4// Defense in depth: re-check loopback even on a verified state
5if (!isAllowedClientRedirect(decoded.r)) return json(400, …);
6
7const out = new URLSearchParams();
8if (code) out.set("code", code);
9out.set("state", decoded.cs);
10return redirect(`${decoded.r}?${out.toString()}`);
POST /oauth/token swaps the client's loopback redirect_uri for the façade's, so Cognito's redirect_uri replay check matches the single registered callback URL:
1if (inForm.has("redirect_uri") || inForm.get("grant_type") === "authorization_code") {
2 inForm.set("redirect_uri", `${base}/oauth/callback`);
3}
Two security properties of this proxy are worth dwelling on, because an OAuth redirect proxy is a textbook open-redirector if you build it carelessly:
- The HMAC is the only authority over the original
redirect_uri. Without it, anyone could forge a state that redirects the auth code to an attacker URL. The façade signs{cs, r, ts}with HMAC-SHA-256 over a base64url payload, with a 10-minute TTL and a 60-second future-skew tolerance. The key is held in SST Secret, never in code. - Loopback-only enforcement, twice.
isAllowedClientRedirect()checks the URL ishttp://localhost,127.0.0.1, or[::1]once at/oauth/authorize(before the state is signed) and a second time at/oauth/callback(after verifying the HMAC). If the HMAC key ever leaks, a forged state still cannot redirect codes anywhere except a loopback address — and PKCE makes the code useless without the verifier.
The state.ts module is fifty lines and has no external dependencies beyond node:crypto. Stateless, no DynamoDB, no TTL bookkeeping. A typical signed state is 150 to 200 bytes, well under Cognito's 1024-character state limit.
What the AgentCore Gateway Side Looks Like (with SST)
The façade is half the architecture. The other half is the AgentCore Gateway you would have built anyway, with one nuance: the user-flow Cognito app client must be in the gateway's allowedClients list alongside the M2M clients.
I use SST v4 for the entire stack. SST is a thin layer over Pulumi that gives you first-class TypeScript primitives for AWS — sst.aws.Function, sst.aws.ApiGatewayV2 — and preserves access to every native Pulumi resource (aws.bedrock.AgentcoreGateway, aws.cognito.UserPool) when SST has not yet shipped a wrapper. That mix matters here because AgentCore Gateway is new enough that there is no sst.aws.AgentcoreGateway yet, but it sits naturally next to the SST-native façade Lambda and HTTP API.
The whole infrastructure is six TypeScript files under infra/. Cognito + IdP federation + per-target M2M clients live in one; the AgentCore Gateway and its targets in another; the façade Lambda + HTTP API in a third. SST's sst.config.ts lazy-imports them in dependency order:
1// sst.config.ts
2async run() {
3 $transform(aws.lambda.Function, (args) => {
4 if (!args.runtime || (typeof args.runtime === "string" && args.runtime.startsWith("nodejs"))) {
5 args.runtime = "nodejs24.x";
6 }
7 });
8
9 await import("./infra/cognito"); // user pool, Feishu IdP, M2M clients
10 await import("./infra/facade-api"); // HTTP API + user-flow Cognito client
11 await import("./infra/gateway"); // AgentCore Gateway + targets
12 await import("./infra/facade"); // façade Lambda + routes
13}
The AgentCore Gateway itself is a native Pulumi resource. The Cognito user-flow client and M2M clients flow into allowedClients as Pulumi Outputs — SST resolves them at deploy time, no manual ARN copying:
1// infra/gateway.ts
2const gateway = new aws.bedrock.AgentcoreGateway("McpGateway", {
3 protocolType: "MCP",
4 authorizerType: "CUSTOM_JWT",
5 authorizerConfiguration: {
6 customJwtAuthorizer: {
7 discoveryUrl: cognitoIssuer.apply(
8 (i) => `${i}/.well-known/openid-configuration`,
9 ),
10 allowedClients: [
11 userAppClient.id, // Claude Code (interactive)
12 backendM2mClient.id, // M2M for the in-account backend
13 // … other M2M clients
14 ],
15 },
16 },
17});
allowedClients is in-place updatable on AgentCore Gateway, so adding a new tenant's M2M client is a one-line edit followed by pnpm -C infra deploy --stage prod — no gateway URL rotation, no client reconfiguration. The user-flow client is generateSecret: true; the façade Lambda — not the browser — holds the secret and forwards it through client_secret_basic on the token endpoint.
On the façade side, SST's first-class primitives keep the wiring tight. The HTTP API and Lambda are both sst.aws.* resources, and the Lambda's environment block carries Outputs straight from the gateway and Cognito modules without intermediate string conversion:
1// infra/facade.ts
2const facadeApi = new sst.aws.ApiGatewayV2("FacadeApi", {
3 cors: {
4 allowOrigins: ["*"],
5 allowMethods: ["*"],
6 allowHeaders: [
7 "Authorization", "Content-Type", "Accept",
8 "MCP-Protocol-Version", // required by browser-based MCP clients
9 ],
10 },
11});
12
13const oauth2FacadeFn = new sst.aws.Function("Oauth2Facade", {
14 handler: "src/lambdas/oauth2-facade/handler.handler",
15 timeout: "30 seconds",
16 memory: "256 MB",
17 environment: {
18 UPSTREAM_GATEWAY_URL: gatewayUrl,
19 COGNITO_ISSUER: cognitoIssuer,
20 COGNITO_AUTHORIZE_ENDPOINT: cognitoAuthorizeEndpoint,
21 COGNITO_TOKEN_ENDPOINT: cognitoTokenEndpoint,
22 USER_CLIENT_ID: userAppClient.id,
23 USER_CLIENT_SECRET: userAppClient.clientSecret,
24 OAUTH_STATE_HMAC_KEY: oauthStateHmacKey.value,
25 // …
26 },
27});
28
29facadeApi.route("ANY /{proxy+}", oauth2FacadeFn.arn);
30facadeApi.route("ANY /", oauth2FacadeFn.arn);
The user-flow Cognito client has exactly one allowed callback URL — the façade's /oauth/callback, set via SST's $interpolate against the HTTP API's URL output:
1// infra/facade-api.ts
2callbackUrls: [$interpolate`${facadeApi.url}/oauth/callback`],
3logoutUrls: [$interpolate`${facadeApi.url}/oauth/logout`],
Every other localhost callback the client opens is reached by the façade's 302, never by Cognito directly.
The HMAC key for opaque-state signing is an sst.Secret, set once per stage with pnpm -C infra exec sst secret set OauthStateHmacKey "$(openssl rand -base64 32)" --stage <stage>. The empty default lets fresh stages deploy clean before the operator seeds it; the Lambda returns 503 on OAuth flows if the key is missing, which is loud enough to catch in a smoke test.
The full stack — including PR-preview stages auto-managed via pr-<N> and a prod stage with protect: true — is what makes this pattern operationally cheap. Adding a new MCP backend is two file edits (a target declaration in infra/gateway.ts, an OpenAPI schema or Lambda handler) and a deploy. The façade is untouched.
Cognito's Federation Story Carries Through
One reason to keep Cognito in this design rather than drop in for Keycloak is that Cognito's native federation surface keeps working untouched by the façade. In the deployment I built, the user pool federates to Feishu via OIDC, but the same pattern applies to any external IdP via Cognito's federated identities — Google, Apple, SAML, your enterprise OIDC.
1const feishuIdp = new aws.cognito.IdentityProvider("FeishuIdp", {
2 userPoolId: cognitoUserPoolId,
3 providerName: "Feishu",
4 providerType: "OIDC",
5 providerDetails: {
6 client_id: feishuAppId,
7 client_secret: feishuAppSecret,
8 authorize_scopes: "openid email profile",
9 oidc_issuer: "https://passport.feishu.cn",
10 authorize_url: "https://passport.feishu.cn/suite/passport/oauth/authorize",
11 token_url: "https://passport.feishu.cn/suite/passport/oauth/token",
12 attributes_url: "https://passport.feishu.cn/suite/passport/oauth/userinfo",
13 jwks_uri: "https://passport.feishu.cn/suite/passport/oauth/userinfo",
14 },
15 …
16});
The MCP client never sees this. It hits the façade's RFC 8414 metadata, follows the authorization_endpoint to the façade, gets 302'd to Cognito's Hosted UI, and Cognito handles the IdP picker. Federation is invisible to the MCP layer, exactly as it should be.
Comparing the Approaches
| Property | Keycloak (full IdP) | AgentCore + Cognito + Façade |
|---|---|---|
| RFC 9728 protected resource metadata | Native (configurable) | Façade serves it |
| RFC 8414 authorization server metadata | Native | Façade serves it |
| RFC 7591 DCR | Native | Façade fakes it (same client to everyone) |
| RFC 8707 audience binding | Workaround via audience mapper | Inherits Cognito's client_id-as-audience model |
| PKCE S256 | Native | Enforced by façade |
| Native loopback redirect | Native | Façade redirect proxy with HMAC state |
| Federation to enterprise IdPs | Native (broad) | Cognito's native federation |
| Operational footprint | ECS Fargate + Aurora Serverless | Two Lambdas + HTTP API + Cognito |
| Cost shape | Per-second container compute | Per-invocation Lambda |
Keycloak gets you a fully MCP-compliant authorization server with a single RFC 8707 workaround. The trade-off is operating Keycloak — a stateful Java application with a Postgres backend.
The façade pattern is a different trade-off: you accept a small amount of TypeScript code in exchange for keeping Cognito and AgentCore Gateway. If you are already on Cognito, already on Bedrock, and your alternative would be re-platforming user identity, the façade is the lower-risk path. The Lambda is ~500 lines and the code surface is small enough to audit in a sitting. There is no database, no long-lived state, and the only secret is the HMAC key for opaque-state signing.
Operational Notes
A few items that took longer than expected to get right:
- The HMAC key is per-stage and stable. Set it once with
sst secret set OauthStateHmacKey "$(openssl rand -base64 32)" --stage <stage>. Rotating it invalidates all in-flight OAuth flows transparently — annoying, not catastrophic. There is no reason to auto-rotate it on every CI deploy. WWW-Authenticaterewriting on the proxy path. When the façade proxies the/mcpendpoint and AgentCore Gateway returns a 401, the upstreamWWW-Authenticateheader points at the gateway URL's RFC 9728 metadata, not the façade's. The façade rewrites it before returning — otherwise discovery walks the client straight back into the AgentCore-fronted version of the metadata, which advertises Cognito as the authorization server and re-opens the original problem.- Token endpoint authentication forwarding. Claude Code sends
client_secret_basic(Authorization header). The proxy forwards that header verbatim to Cognito. Without forwarding, Cognito sees no client credentials and returnsinvalid_client(HTTP 400) — a confusing failure mode the first time you hit it. MCP-Protocol-Versionin the CORS allowlist. The façade's HTTP API needsMCP-Protocol-VersioninallowHeaders, otherwise browser-based MCP clients (the Inspector) get blocked by preflight before any of the auth flow runs.
Conclusion
AgentCore Gateway is a well-designed multi-target MCP runtime with a clean JWT authorization model. Cognito is a well-designed OIDC identity provider. Neither was built with the full MCP authorization spec in mind — that spec sits on RFC 9728 / 8414 / 7591 / 7636 in a way that overlaps but does not match either component's native surface.
A 500-line API Gateway + Lambda façade closes the four gaps: it serves the metadata documents the spec wants, fakes DCR by returning the pre-provisioned Cognito client, and proxies the authorization-code flow through a single registered callback URL with HMAC-signed opaque state for native-app loopback redirects. AgentCore Gateway and Cognito remain unmodified.
Where Keycloak gets you a full identity stack at the cost of operating one, this pattern lets you stay on managed Cognito and AgentCore Gateway and still hand a claude mcp add <https-url> to a teammate. For deployments that already live in this corner of AWS, the façade is the minimum viable adapter.
The whole pattern is small enough that the snippets above plus the architecture diagram are the entire load-bearing surface — the rest is SST scaffolding, an interceptor Lambda for tool-level scope gating, and the usual CI plumbing.
Resources
MCP and OAuth Specifications
- Model Context Protocol Authorization Specification — the full MCP authorization spec, including the RFC stack
- RFC 6749 — OAuth 2.0 Authorization Framework — including the
redirect_uriexact-match rule - RFC 7591 — OAuth 2.0 Dynamic Client Registration
- RFC 7636 — Proof Key for Code Exchange (PKCE)
- RFC 8252 — OAuth 2.0 for Native Apps — loopback redirect URI rules
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 8707 — Resource Indicators for OAuth 2.0
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
AWS Documentation
- Amazon Bedrock AgentCore Gateway — managed multi-target MCP runtime
- Amazon Cognito User Pools — federated identity provider
- Cognito Federated Identities — connecting to external IdPs
Related Articles
- Technical Deconstruction of MCP Authorization: A Deep Dive into OAuth 2.1 and IETF RFC Specifications — the underlying RFC stack and IdaaS compatibility matrix
- Implementing MCP OAuth 2.1 with Keycloak on AWS — the comparison case: a full IdP with one workaround
- MCP OAuth Evolution: SEP-991 Simplifies Client Registration — where DCR is going
- How invoking remote MCP servers hosted on AWS AgentCore — earlier work on the AgentCore client side
- Leveraging MCP Client's OAuthClientProvider for Seamless AWS AgentCore Authentication — using the MCP SDK's native OAuth client