npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

serverless-plugin-mcp-cognito-proxy

v0.2.1

Published

Serverless Framework plugin: registration-free, stateless OAuth proxy (DCR + CIMD) in front of an existing AWS Cognito pool.

Readme

serverless-plugin-mcp-cognito-proxy

A Serverless Framework plugin that puts a stateless, registration-free OAuth 2.1 façade in front of an existing AWS Cognito user pool, so MCP clients (Claude Code, Claude Desktop, Cursor, …) can log in through Cognito without you pre-registering each client.

Why

The MCP authorization spec expects an authorization server that offers:

  • RFC 8414 metadata at /.well-known/oauth-authorization-server
  • RFC 7591 Dynamic Client Registration (/register) and/or Client-ID-Metadata-Documents (CIMD)
  • loopback redirect_uris on arbitrary ports (native clients)

Cognito provides none of these: it only serves OIDC discovery, has no DCR, and requires every callback URL to be registered on an app client up front. This plugin deploys a small Lambda that fills exactly that gap and delegates the actual login and token issuance to Cognito.

What it deploys

| Resource | Purpose | |---|---| | McpProxyUserPoolClient | A confidential Cognito app client (code flow, hosted UI) whose callback is the proxy. Its generated secret is also the proxy's sealing key. | | mcpCognitoProxy Lambda | Hono app serving /.well-known/*, /register, /authorize, /auth/callback, /token. | | mcpProxyJwt authorizer (API Gateway mode only) | An HTTP API JWT authorizer validating Cognito access tokens issued to that app client, attached to your protectedRoutes. |

That's it: no DynamoDB table, no SSM parameters, no IAM statements. The proxy is fully stateless.

How it works

MCP client                proxy (this plugin)                    Cognito
   │  GET /.well-known/…      │                                      │
   │─────────────────────────▶│  metadata: issuer = proxy,           │
   │                          │  jwks_uri = Cognito                  │
   │  POST /register          │                                      │
   │─────────────────────────▶│  client_id = sealed(metadata) ─┐     │
   │  GET /authorize          │                                │     │
   │─────────────────────────▶│  state = sealed(txn) ──────────┼────▶│ hosted UI login
   │                          │◀── /auth/callback?code&state ──┼─────│
   │◀─ redirect_uri?code ─────│  code = sealed(cognito code)   │     │
   │  POST /token (+PKCE)     │                                │     │
   │─────────────────────────▶│  open code, check PKCE ────────┼────▶│ /oauth2/token
   │◀─ Cognito tokens ────────│◀───────────────────────────────┼─────│

Every piece of in-flight state — the DCR registration, the authorization transaction, the authorization code — travels with the client as an AES-256-GCM sealed blob (JWE, key derived from the app client secret via HKDF). The Cognito code is exchanged only at /token time, so Cognito enforces single use. refresh_token grants are forwarded to Cognito, so long-lived sessions work.

By default the tokens returned to the client are Cognito's own. With tokenIssuer: proxy the access token is re-minted under the proxy's issuer (copying sub, scope, cognito:groups, username, client_id, adding mcp_client_id and aud from the RFC 8707 resource), signed with an ES256 key derived from the same secret — still nothing stored.

Install

npm install --save-dev serverless-plugin-mcp-cognito-proxy
plugins:
  - serverless-plugin-mcp-cognito-proxy

Configuration

custom:
  mcpCognitoProxy:
    userPoolId: us-east-1_xxxxxxx                                # required
    cognitoDomain: https://your-pool.auth.us-east-1.amazoncognito.com  # required (hosted UI domain)
    issuerUrl: https://api.example.com          # optional: defaults to the deployed HTTP API endpoint
    protectedRoutes: [/mcp]                     # optional: omit for standalone AS mode (see below)
    scopes: [openid, email, profile]            # optional: default shown
    tokenIssuer: cognito                        # optional: cognito (default) | proxy — see Mode 2

| Option | Description | |---|---| | userPoolId | Existing pool, <region>_<id>. The Cognito issuer is derived from the region prefix. | | cognitoDomain | The pool's hosted-UI domain (Cognito-prefix or custom). | | issuerUrl | Public base URL of the proxy. Leave unset unless you front the API with a custom domain; the plugin derives it from HttpApi.ApiEndpoint in a single deploy. | | protectedRoutes | HTTP API paths in this service to put behind the JWT authorizer. | | scopes | Scopes requested from Cognito and allowed on the app client. | | tokenIssuer | cognito: clients get Cognito's tokens unchanged. proxy: /token re-mints the access token as an ES256 JWT under the proxy's issuer (P-256 key derived from the app client secret; JWKS served at /.well-known/jwks.json). Refresh and ID tokens are Cognito's in both modes. |

Mode 1: protecting routes on API Gateway (HTTP API)

Use this when your MCP server is a Lambda in the same Serverless service, behind an HTTP API.

custom:
  mcpCognitoProxy:
    userPoolId: ${env:USER_POOL_ID}
    cognitoDomain: ${env:COGNITO_DOMAIN}
    protectedRoutes:
      - /mcp

functions:
  mcp:
    handler: handler.mcp
    events:
      - httpApi: { method: "*", path: /mcp }

The plugin:

  1. serves /.well-known/oauth-protected-resource[/mcp] naming the proxy as the authorization server;
  2. injects an HTTP API JWT authorizer with issuerUrl = the Cognito pool issuer and audience = the injected app client id (API Gateway accepts Cognito's client_id claim in place of aud);
  3. attaches it to every httpApi event whose path is in protectedRoutes.

Your handler receives the Cognito access token claims in event.requestContext.authorizer.jwt.claims (sub, cognito:groups, scope, …).

Verify after deploy:

curl -s $ENDPOINT/.well-known/oauth-protected-resource/mcp | jq
curl -s -o /dev/null -w "%{http_code}\n" -X POST $ENDPOINT/mcp   # 401
claude mcp add --transport http example $ENDPOINT/mcp             # then /mcp in Claude Code

Mode 2: authorization server for Amazon Bedrock AgentCore Gateway

AgentCore Gateway already does the resource-server half: it returns 401 with a WWW-Authenticate pointing at its own /.well-known/oauth-protected-resource, and validates JWTs via its customJwtAuthorizer. What it lacks is an authorization server that MCP clients can discover and register with — Cognito on its own fails at RFC 8414/7591 (awslabs/agentcore-samples#1056). This plugin provides that AS.

Deploy the proxy with no protectedRoutes and tokenIssuer: proxy:

service: mcp-auth
provider: { name: aws, runtime: nodejs20.x }
plugins:
  - serverless-plugin-mcp-cognito-proxy
custom:
  mcpCognitoProxy:
    userPoolId: ${env:USER_POOL_ID}
    cognitoDomain: ${env:COGNITO_DOMAIN}
    tokenIssuer: proxy

Then point the Gateway's inbound authorizer at the proxy (CloudFormation shown; the console/CLI take the same values — see examples/agentcore-gateway for a complete stack including a Lambda tool target):

Gateway:
  Type: AWS::BedrockAgentCore::Gateway
  DependsOn: [HttpApiStage, McpCognitoProxyLambdaFunction]   # the Gateway fetches discovery on creation
  Properties:
    Name: my-gateway
    ProtocolType: MCP
    AuthorizerType: CUSTOM_JWT
    RoleArn: !GetAtt GatewayRole.Arn
    AuthorizerConfiguration:
      CustomJWTAuthorizer:
        DiscoveryUrl: !Join ["", [!GetAtt HttpApi.ApiEndpoint, "/.well-known/openid-configuration"]]
        AllowedClients: [!Ref McpProxyUserPoolClient]
  • DiscoveryUrl → the proxy. The Gateway derives its protected-resource metadata from it, so clients discover the proxy (with DCR/CIMD) instead of Cognito.
  • AllowedClients matches the client_id claim, which proxy-minted tokens copy from Cognito's token: the injected app client id.

Why tokenIssuer: proxy is required here (verified against a live Gateway): the Gateway compares the token's iss to the issuer in the discovery document, and reports a mismatch as 403 insufficient_scope. The same Cognito token is accepted when the Gateway's discovery URL is Cognito's — but then the Gateway advertises Cognito as the AS and MCP clients can't register. Re-minting under the proxy's issuer satisfies both. AgentCore accepts the ES256 signature.

Gotcha: the Gateway caches the discovery document it fetched at creation. If you switch tokenIssuer on an existing deployment, the Gateway keeps validating against the old jwks_uri (401 invalid_token) until it is updated or recreated (aws bedrock-agentcore-control update-gateway … with the same configuration is enough).

Security model

Be deliberate about what you are trusting when you deploy this:

  • The Cognito app client secret is the root of trust. The sealing key (client registrations, transactions, codes) and, in tokenIssuer: proxy mode, the ES256 signing key are both derived from it via HKDF with distinct labels. The secret is visible in the proxy Lambda's environment configuration, so anyone with lambda:GetFunctionConfiguration (or CloudFormation read access to the resolved stack) in your account can forge registrations, codes, and — in proxy mode — access tokens. Scope those IAM permissions accordingly. Rotating the app client secret invalidates all outstanding registrations, codes, and proxy-issued tokens at once.
  • Authorization codes are single-use because the inner Cognito code is exchanged only at /token time and Cognito enforces single use; the proxy's sealed code additionally expires after 2 minutes and is PKCE-bound (S256 required, verified with a constant-time compare).
  • Login and consent stay with Cognito. The proxy never sees passwords; it holds no tokens at rest (it holds nothing at rest — there is no datastore to breach).
  • DCR registrations expire after 30 days (the sealed client_id carries its own expiry). MCP clients handle this by re-registering; other clients must tolerate it.
  • CIMD fetches are defensive: https:// client_ids are fetched without following redirects, with a 5s timeout, a JSON content-type requirement, and a 64KB size cap, and the document must echo the client_id and use only https or loopback redirect URIs.
  • /register is unauthenticated by design (RFC 7591 open registration). Because registration writes nothing, it cannot be used to fill a database; a registration is only a signed statement of redirect URIs.

Compatibility

  • Serverless Framework v4 (v3 is untested).
  • API Gateway HTTP API (httpApi events) only — not REST API (http events).
  • An existing Cognito user pool with a hosted-UI domain (Cognito-prefix or custom).
  • The proxy Lambda runs on nodejs20.x.
  • Verified against MCP clients using OAuth discovery + DCR/CIMD with PKCE S256; grants: authorization_code, refresh_token.

Client support

  • Redirect URIs (both DCR and CIMD): https:// anywhere, or http:// on a loopback host (localhost, 127.0.0.1, [::1]). Loopback URIs match regardless of port (RFC 8252 §7.3), so a client can register http://localhost/callback and redirect to http://localhost:51234/callback — this is what Claude Code does.
  • DCR (RFC 7591): client_id is a self-contained sealed blob valid for 30 days.
  • CIMD: an https:// client_id (e.g. Claude Code's https://claude.ai/oauth/claude-code-client-metadata) is fetched and its document must echo the client_id and use allowed redirect URIs.
  • PKCE S256 required; token_endpoint_auth_method: none.
  • Grants: authorization_code, refresh_token.

Examples

examples/ contains deployable stacks for both modes plus a shared Cognito pool. Install the plugin into them from a packed tarball — a file:../.. link makes Serverless's packager follow the symlink back into the repo and loop until it runs out of memory:

npm run build && npm pack --pack-destination examples
cd examples/cognito && npx serverless deploy          # shared pool + hosted UI domain
cd ../api-gateway && npm install && npx serverless deploy
cd ../agentcore-gateway && npm install && npx serverless deploy

Development

npm install
npm test          # vitest: plugin + runtime
npm run typecheck
npm run build     # dist/ (plugin, CJS) + dist/runtime/index.mjs (Lambda bundle, esbuild)

The Lambda bundle ships inside the npm package and is copied into the consumer's service as .mcp-proxy/ during serverless package (and removed afterwards). Add .mcp-proxy/ to your .gitignore.