OAuth 2.1 for your MCP server, so nobody pastes a shared API key into a client config.
Get started · Documentation · Threat model · Conformance matrix · Verification status
A remote MCP server needs to know who is calling. The usual answer is a static API key pasted into every client configuration. One key gives every caller the same identity, never expires, and leaks in a git add . or a support screenshot. Revoking it means rotating it for everyone.
MCP's answer is OAuth 2.1. That leaves a gap. An MCP client must identify itself to an authorization server and obtain a token that is valid for exactly your server. Identity providers such as Microsoft Entra ID and Cloudflare Access prove who the user is, but they do not speak the MCP client-facing flow: client registration, PKCE, consent, and audience-bound tokens.
mcp-sso fills that gap. It is a TypeScript library in two halves:
- The authorization-server bridge accepts the client's identity (a Client ID Metadata Document, or
POST /oauth/register), sends the user to your identity provider, shows a consent page, and mints its own access and refresh tokens for one MCP resource. - The resource-server verifier checks the token's signature, audience, and scope on every
/mcprequest, and answers with the RFC 9728WWW-Authenticatechallenge when it rejects one.
Your identity provider's tokens stay between the provider and mcp-sso. The MCP client only ever holds a token that mcp-sso minted for your server.
npm install mcp-ssoNode 24 or later. jose is the only runtime dependency. Fastify, Express, Hono, mysql2, ioredis, and @fastify/rate-limit are optional peer dependencies: install the ones you use.
This runs a local server that uses console pairing as the identity check. It listens on loopback and refuses a non-loopback host, issuer, or resource before it writes any state.
npx mcp-sso init my-mcp-server
cd my-mcp-server
npm install
npm startIn another terminal, add it to an MCP client. With Claude Code:
claude mcp add --transport http my-bridge http://127.0.0.1:3000/mcpThe server prints a one-time code. Paste it into the browser, approve, and call a tool.
Warning
Anyone who can read that code becomes the console-operator identity. Keep the server on loopback and its console private. This is a tutorial, not a production template. For real users, follow the identity-provider tutorial.
Assume Alice connects an MCP client to https://mcp.example/mcp.
- The client identifies itself with a Client ID Metadata Document (CIMD) or by calling
POST /oauth/register. -
mcp-ssosends Alice to your identity provider. The provider proves who she is. - Alice sees the client, the resource, and the requested scopes, and approves.
- The client exchanges the authorization code and its PKCE verifier for tokens. The access token is valid for
https://mcp.example/mcpand nothing else. - Your MCP server verifies the token's audience and required scope on every request. Refresh-token rotation detects a replayed refresh token and revokes the whole token family.
sequenceDiagram
participant C as MCP client
participant B as mcp-sso
participant I as Identity provider
participant S as MCP server
C->>B: CIMD identity or POST /oauth/register
C->>B: Authorization request with PKCE
B->>I: Sign Alice in
I-->>B: Verified identity
B-->>C: Consent, then authorization code
C->>B: Authorization code and PKCE verifier
B-->>C: Tokens for mcp.example/mcp
C->>S: MCP request with access token
S->>S: Verify audience and required scope
How the OAuth roles fit together explains each step and where a request can stop.
The bridge issues tokens. The verifier is what your /mcp handler calls. This fragment shows the shape; the complete, tested composition is examples/fastify-sqlite/.
import { RequestAuthorizer, buildUnauthorizedChallenge, OAuthError, SystemClock, noopAudit } from "mcp-sso";
// `config` is the BridgeConfig you built with createBridgeConfig().
const authorizer = new RequestAuthorizer({ config, clock: new SystemClock(), audit: noopAudit });
app.post("/mcp", async (request, reply) => {
let auth;
try {
auth = await authorizer.authorize({ authorization: request.headers.authorization, requiredScope: "mcp:read" });
} catch (error) {
const oe = error instanceof OAuthError ? error : new OAuthError("invalid_token", "Bearer token is invalid", 401);
reply.header("www-authenticate", buildUnauthorizedChallenge(config, { scope: config.scopeCatalog, error: oe.code, errorDescription: oe.message }));
return reply.code(oe.status).send({ jsonrpc: "2.0", error: { code: -32001, message: `${oe.code}: ${oe.message}` }, id: null });
}
// auth.subject, auth.scopes, and auth.credentialKind ("interactive" or "machine") are verified here.
});RequestAuthorizer.authorize fails closed. A missing token, a wrong audience, an expired token, or a missing scope throws an OAuthError whose status is 401 or 403. There is no local or unauthenticated mode. The tested example reads the header through headersFromDistinct so that a duplicated Authorization header is rejected instead of silently collapsed; this fragment reads it the plain way. Start from the example when you wire your own route.
| Area | Current source |
|---|---|
| Identity options | Cloudflare Access, Microsoft Entra ID (redirect flow plus group-to-scope ceilings), Google, generic OIDC, console pairing for local use, and claims-only website login |
| Client registration | CIMD, and POST /oauth/register in stateless or stored mode |
| Frameworks | Fastify, Express, Hono. OAuth decisions stay in the framework-free core |
| Stores |
node:sqlite (one host), MySQL (shared replicas), memory (one process). One shared conformance suite, also published as mcp-sso/testing/*
|
| Grants | Authorization code with PKCE S256, refresh-token rotation with family revocation, client_credentials for machine clients |
| Operations | JSONL and webhook audit sinks, Redis or Valkey rate limiting, a Fastify /mcp rate-limit helper |
| Runtime dependency |
jose, and nothing else |
Not shipped: a GitHub identity port and the device authorization grant are contract-only. Neither is a release claim. The capability reference has the exact list and the deployment limits, including that one bridge protects one resource.
Security products earn trust with artifacts, not adjectives. These are the ones this project publishes.
- A STRIDE threat model with 49 attacker-driven rows, the control for each, and the residual risk that remains.
- A conformance matrix against MCP Authorization
2026-07-28and CIMD draft-00, row by row, with the two deviations recorded rather than hidden. - A release verification matrix that binds every shipped feature to a test row, plus packed-artifact checks, plus a dated live client matrix against real identity providers and real MCP clients.
- A dependency ledger:
joseis the only runtime dependency, every pin is recorded with its publish date, and ordinary updates wait 15 days. npm publishes run only from GitHub Actions with OIDC provenance, never from a laptop. - Fail-closed defaults. Ambiguous configuration, a missing identity, an unknown audience, or a replayed refresh token is a hard failure, never a degraded default. Authorization codes and refresh tokens are stored as hashes and are single-use. A stolen access token is a bearer token and stays valid until
exp, which is why it is short-lived (threat model row 1).
Use mcp-sso when your identity provider authenticates users but does not expose MCP-compatible OAuth endpoints.
| Project | Choose it if |
|---|---|
mcp-sso (this repo) |
Your identity provider is not an MCP authorization server: Entra ID, Cloudflare Access, most enterprise SSO. |
mcp-auth |
Your identity provider already exposes compatible OAuth 2.1 client registration and you only need resource-server wiring (compatibility list). |
mcp-oauth-server |
You need the device authorization grant today. mcp-sso ships client_credentials, not device flow. |
workers-oauth-provider |
Your MCP server is a Cloudflare Worker. |
The documentation index routes by what you are trying to do. The short version:
- Learn: Get started.
- Do: configure client registration, provision a machine client, put SSO in front of an API-key backend, ship audit events, run live verification.
- Look up: configuration, contracts, threat model, verification status.
- Understand: OAuth roles and flow, client registration choices, redirect URI trust, rate limits and client IP trust, CIMD fetch safety.
MIT