Library / SDK
cloudflare/workers-oauth-provider avatar
cloudflare/workers-oauth-provider

Split your MCP auth server from the Worker that checks the token

OAuth provider library for Cloudflare Workers

1,888 stars141 forksTypeScriptMIT

At a glance

What is it?
@cloudflare/workers-oauth-provider turns a Cloudflare Worker into an OAuth 2.1 authorization server for HTTP APIs and remote MCP servers. Its current recommended shape is two Workers, where the token check crosses a Service Binding and your handler still owns scope enforcement.
Who is it for?
Adopt this library if you are serving a remote MCP endpoint or an HTTP API from a Cloudflare Worker and want authorization server plumbing you do not have to write. Do not adopt it if your API lives outside Workers, or if you expect the library to enforce scopes for you, because it advertises requiredScopes and leaves the check to ctx.auth.scope in your handler.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two roles, two Workers, one Service Binding between them

The recommended architecture in this library has two roles. The authorization server signs users in and issues tokens. The resource server is your MCP endpoint: it accepts those tokens and verifies them by calling the authorization server over a Cloudflare Service Binding.

That split is the headline design change in the current line of the library, and it maps directly onto the two classes you instantiate. `OAuthAuthorizationServer` handles the protocol side: discovery, the token endpoint and revocation, plus `validateToken` for callers that hold a binding to it. `OAuthResourceServer` is what you export from the Worker that actually serves MCP.

The resource side declares who it is and what it needs:

ts
export default new OAuthResourceServer<Env, AuthProps>({
  resourceMetadata: {
    resource: 'https://mcp.example.com/mcp',
    authorization_servers: ['https://auth.example.com'],
  },
  requiredScopes: ['mcp:read'],
  validateToken: (env) => env.AUTH_SERVER.validateToken,
  handler: {
    fetch(request, env, ctx) {
      const needed = request.method === 'GET' ? ['mcp:read'] : ['mcp:read', 'mcp:write'];
      if (!needed.every((scope) => ctx.auth.scope.includes(scope))) return insufficientScope(ctx.auth, needed);
      return Response.json({ userId: ctx.props.userId });
    },
  },
});

Three things in that snippet carry the design. The resource publishes its RFC 9728 metadata. It answers unauthenticated requests with a challenge pointing at that metadata, which is how an MCP client discovers where to authorize. And it accepts only tokens issued for its own resource.

The quick start validates tokens by calling `env.AUTH_SERVER.validateToken` through the Service Binding.

requiredScopes is advertised, not enforced

This is the single most important thing to understand before you write a handler, and the README states it plainly.

Two different lists appear on the wire under the same name, `scopes_supported`, and the spec naming is what makes them confusing. `scopesSupported` on the authorization server is everything that server can grant. A resource's `requiredScopes` is what any access needs, so MCP clients request it first, and anything beyond it comes by step-up.

The library advertises the required scopes but does not enforce them, and the reason is stated: only your code knows which scopes imply others. So the enforcement loop is yours to write. The quick start shows the intended pattern, where a GET needs `mcp:read` and anything else needs `mcp:read` plus `mcp:write`, and a failure returns a 403 naming the missing scope so the client re-authorizes for it. That is step-up authorization in its simplest form, and the README links a dedicated scopes and step-up section in the authorization server reference.

Two handler inputs come from the framework. `ctx.props` is whatever `completeAuthorization()` stored at consent time. `ctx.auth` is the verified token, and `ctx.auth.scope` is the list you test against.

The `/authorize` endpoint is entirely yours in the split shape. The README comment in the quick start names the three steps: parse the auth request with `parseAuthRequest()`, sign the user in and ask for consent, then call `completeAuthorization()`. Everything that is not the `/authorize` path goes to the authorization server's own fetch, which serves discovery, token and revocation.

KV holds only hashes, and props are sealed to the token holder

The storage layer has one binding, and it holds the authorization server's state. The README states the authorization server needs a KV namespace bound as `OAUTH_KV`.

What goes in that namespace is specified in `storage-schema.md`. Tokens, codes and secrets are stored only as hashes, which means a leaked KV dump does not yield usable credentials. The `props` payload is different: it is encrypted with a key only the token holder can unwrap. That design lets you keep user identity or tenancy data beside the token without making the KV namespace readable enough to reconstruct it.

That coupling between `props` and the token is worth internalising, because it is why `ctx.props` is safe to hand to your handler. A revoked token cannot have its props decrypted by anyone else, since the key never left the token.

If you also accept Client ID Metadata Documents, the README asks for the `global_fetch_strictly_public` compatibility flag as well. That is an extra configuration step that is easy to miss, and it belongs with the KV binding in your setup checklist.

The single Worker shape still exists, and is not the recommended one

If you are upgrading, the class you may know from 0.x is `OAuthProvider`, and it is still fully supported. It combines both roles in one Worker, which is what it was designed for.

The routing is by path. Requests under `apiRoute` reach `apiHandler`, and everything else goes to `defaultHandler`, which owns the `/authorize` page and reaches the helpers through `env.OAUTH_PROVIDER`:

ts
export default new OAuthProvider<Env>({
  apiRoute: '/mcp',
  apiHandler: McpApiHandler,
  defaultHandler,
  authorizeEndpoint: '/authorize',
  tokenEndpoint: '/oauth/token',
  scopesSupported: ['mcp:read', 'mcp:write', 'offline_access'],
  resourceMetadata: {
    resource: 'https://mcp.example.com/mcp',
    authorization_servers: ['https://mcp.example.com'],
  },
  requiredScopes: ['mcp:read'],
  clientIdMetadataDocumentEnabled: true,
});

Note the difference in `resourceMetadata.authorization_servers`: in the single Worker shape the authorization server is the same host as the MCP endpoint.

The README is explicit that the split roles are the recommended shape and that `OAuthProvider` is the older 0.x arrangement. If you are starting fresh on an MCP server, the two Worker layout with a Service Binding is the one to copy, because it is what the examples and docs describe. There is a full working pair in `examples/split-workers`, including the `/authorize` handler, a `wrangler.jsonc`, and an end to end test that runs both Workers in workerd.

The version story matters here. The README leads with a migration note: upgrading from 0.x, 1.0 or 1.1 has a guide at `docs/migration-1.x.md` covering every change since 0.10 with code, and a copy of that guidance ships inside the npm package under `skills/migrate-to-1.x/`.

Standards coverage and what the conformance suite tests

The library positions itself against a specific list of documents, and the list is the best available description of its scope.

The core is OAuth 2.1 at draft-ietf-oauth-v2-1-13, plus the bearer token RFC 6750, token revocation RFC 7009, the assertion framework RFC 7591, PKCE RFC 7636, authorization server metadata RFC 8414, token exchange RFC 8693, security best current practice RFC 8707, and resource indicators RFC 9728. On top sit Client ID Metadata Documents at draft-ietf-oauth-client-id-metadata-document-00 and the OpenID Connect RP Metadata Choices specification.

The one that matters most for MCP deployments is the MCP authorization specification dated 2026-07-28, since that is the document a remote MCP client is written against. Enterprise-Managed Authorization is listed as experimental.

The test scripts in `package.json` show how seriously that is taken. `npm run test` runs vitest, `npm run test:conformance` runs vitest against the `conformance/` directory, and `npm run test:conformance:upstream` invokes `conformance/upstream/run.mjs` directly. The conformance dependency is `@modelcontextprotocol/conformance` at `0.2.0-alpha.11`, so the upstream suite is pinned to an alpha. That is a reasonable trade for MCP work and it is also a reminder that the conformance target itself is still moving.

Building it locally, and where this fits against the alternatives

Development needs Node 24 or newer, followed by `npm install`, `npm run build` and the test commands. The build is tsdown, typechecking is `tsc`, and `npm run check` runs both typecheck and test together. Formatting is prettier. Releases go through changesets, given the `.changeset/` directory and the changelog GitHub action dependency.

The published package is deliberately narrow. The `files` list in `package.json` is `dist`, `docs` and `skills`, so the documentation and the migration skill ship with the library rather than living only on GitHub. That is a nice touch for anyone whose coding agent needs the migration instructions offline.

The alternatives are worth naming concretely. Building OAuth by hand on a Worker means implementing discovery, PKCE, the token endpoint, revocation and RFC 9728 metadata yourself, which is exactly the work this package removes. Using a hosted identity provider means your tokens and your consent decisions live in someone else's product and your MCP metadata still has to point at it, so the RFC 9728 discovery step does not go away. A general purpose auth library that is not Worker specific would give you OAuth without the MCP resource metadata and discovery behaviour.

The licence is MIT, recorded in `package.json` and in a `LICENSE.txt` file at the repository root. The repository is not archived, and the last push was on 2026-10-07, after the v1.2.2 release on 2026-10-06 and v1.2.1 release on 2026-09-28. The recent releases show an active version cadence, so pin the version rather than tracking latest.

Editorial conclusion

Adopt this library if you are serving a remote MCP endpoint or an HTTP API from a Cloudflare Worker and want authorization server plumbing you do not have to write. Do not adopt it if your API lives outside Workers, or if you expect the library to enforce scopes for you, because it advertises requiredScopes and leaves the check to ctx.auth.scope in your handler. Verify first that you can bind a KV namespace as OAUTH_KV, enable the global_fetch_strictly_public compatibility flag if you accept Client ID Metadata Documents, and read docs/migration-1.x.md before porting from 0.x.

Frequently asked questions

How do I install @cloudflare/workers-oauth-provider and what bindings does it need?

Run npm install @cloudflare/workers-oauth-provider. The authorization server needs a KV namespace bound as OAUTH_KV, and if it accepts Client ID Metadata Documents it also needs the global_fetch_strictly_public compatibility flag.

Does workers-oauth-provider enforce scopes for me?

No. It advertises requiredScopes so clients request them, but enforcement stays in your handler, where you check ctx.auth.scope. The reason given is that only your own code knows which scopes imply others.

How does the split authorization server and resource server shape work?

The authorization server signs users in and issues tokens, and the resource server is your MCP endpoint. The resource server validates tokens by calling the authorization server over a Service Binding, using validateToken: (env) => env.AUTH_SERVER.validateToken.

What happens to my data when an MCP client sends an unauthenticated request?

The resource server answers with a challenge pointing at its RFC 9728 resource metadata, which is how the client finds the authorization server. It accepts only tokens issued for its own resource.

Is OAuthProvider deprecated or still supported?

Still fully supported for a single Worker that is both the authorization server and its only resource, which was the 0.x shape. The README calls the split two Worker roles the recommended shape, and points upgrading users at docs/migration-1.x.md.

Official sources

  1. cloudflare/workers-oauth-provider on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Community

Where developers are discussing it

Posts on Hacker News, dev.to and Lobsters that link to this repository, found by our scan of those communities. 1 in the last 30 days; newest first.

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cloudflare-workers-oauth-provider.svg)](https://hysenlabs.com/projects/cloudflare-workers-oauth-provider)

Was this useful?