-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(examples): deploy todos-server to Cloudflare Workers with OAuth and a live board #2446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
+2,642
−530
Closed
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
2ab9fd1
feat(examples): deploy todos-server to Cloudflare Workers
felixweinberger 2b1bbc7
feat(examples): OAuth via workers-oauth-provider and a live board vie…
felixweinberger ba7b01e
feat(examples): live view of OAuth boards, claimed at consent
felixweinberger 376ce7a
examples: point clients at the live board view
felixweinberger 4fab4f4
examples: the live board says who you are
felixweinberger 722f491
examples: show the live-board link the moment a client connects
felixweinberger a557141
examples: consent click opens the live board
felixweinberger e437779
fix(examples): rewrite the board page script; a stale element referen…
felixweinberger 23eaba9
fix(examples): consent-opened board tab waits for the approval to land
felixweinberger 805a124
fix(examples): the board view route never routes by session id
felixweinberger f29f238
refactor(examples): one door to a board — serveBoard owns the whole t…
felixweinberger 2f69a02
docs(examples): how to verify a deployment
felixweinberger 4b4738b
refactor(examples): iteration-2 deletions and truth fixes
felixweinberger 72f8b76
chore(examples): lockfile entry for the workers-oauth-provider depend…
felixweinberger f408798
docs(serving): bring your own Authorization Server
felixweinberger f49dec5
docs: iteration-3 prose fixes — handlers read ctx.http.authInfo, READ…
felixweinberger 7afd80b
examples: render board tasks as minimal cards
felixweinberger 3512aae
fix(examples): cancellation on Workers needs enable_request_signal
felixweinberger 4252d5e
examples: real @cloudflare/workers-types instead of structural shims
felixweinberger 9d7afb4
examples: deliver the echo — drop origin tracking from the bus fan-out
felixweinberger 80909ca
examples: adopt the protected Hono wiring for the todos Node entry
felixweinberger f6ae355
docs: re-sync web-standard fences after rebase
felixweinberger c3a1900
examples: serve a Client ID Metadata Document for the demo's own test…
felixweinberger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| --- | ||
| shape: how-to | ||
| description: Integrate an MCP server with an Authorization Server you bring — a platform provider, an auth framework, or your IdP. | ||
| --- | ||
|
|
||
| # Bring your own Authorization Server | ||
|
|
||
| Protecting a server with a token gate → [Require authorization](./authorization.md). Signing a user in from a client → [Authenticate a user with OAuth](../clients/oauth.md). This page covers the other half of the deployment: where the Authorization Server itself comes from, and how the SDK connects to it. | ||
|
|
||
| The SDK ships the Resource Server pieces only — verification, challenges, discovery — and no Authorization Server, deliberately: token issuance, consent, and client registration belong to dedicated systems. The AS you bring is typically your platform's (Cloudflare's [`workers-oauth-provider`](https://github.com/cloudflare/workers-oauth-provider)), an auth framework's (better-auth, in the [`oauth` example](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/oauth)), or your organization's IdP. Two integration styles cover all of them. | ||
|
|
||
| ## Style A: the Authorization Server fronts your handler | ||
|
|
||
| The AS wraps your deployment and verifies every API request before your code runs — `createMcpHandler` performs no verification of its own, so the integration is one option: map the identity the AS hands you into an `AuthInfo` and pass it in. | ||
|
|
||
| ```ts source="../../examples/guides/serving/external-authorization-servers.examples.ts#styleA_injectAuthInfo" | ||
| // The fronting provider verified the token and hands your handler the identity | ||
| // it stored at grant time (workers-oauth-provider: `ctx.props`). Map it. | ||
| interface GrantProps { | ||
| clientId: string; | ||
| scopes: string[]; | ||
| } | ||
|
|
||
| async function serveVerified(request: Request, props: GrantProps): Promise<Response> { | ||
| const authInfo: AuthInfo = { | ||
| token: request.headers.get('authorization')?.replace(/^Bearer /i, '') ?? '', | ||
| clientId: props.clientId, | ||
| scopes: props.scopes | ||
| // No expiresAt: the provider enforces validity on every request. | ||
| }; | ||
| return handler.fetch(request, { authInfo }); | ||
| } | ||
| ``` | ||
|
|
||
| Every tool and resource handler now reads the identity as `ctx.http.authInfo`, and token expiry and revocation are enforced by the provider on each request — your code never sees an invalid token. One contract to know: fronting providers typically hand your handler only what was stored when the grant was approved, so embed everything `AuthInfo` needs (client id, scopes) into the grant at consent time. | ||
|
|
||
| ::: info Running reference | ||
| The [`todos-server` example](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/todos-server) deploys this style on Cloudflare Workers: `workers-oauth-provider` owns the endpoints, both discovery documents, dynamic client registration and Client ID Metadata Documents, and KV-backed grants — the app supplies a consent step and the `propsToAuthInfo` mapping (`oauth.ts`), and serves anonymous and token-authorized tiers side by side. | ||
| ::: | ||
|
|
||
| ## Style B: the SDK verifies tokens from an external AS | ||
|
|
||
| When the AS is elsewhere — an IdP issuing JWTs, or any issuer reachable for introspection — your server verifies each request itself: implement `OAuthTokenVerifier` against the issuer and put `requireBearerAuth` in front of the handler. | ||
|
|
||
| ```ts source="../../examples/guides/serving/external-authorization-servers.examples.ts#styleB_externalVerifier" | ||
| // The AS is external (an IdP issuing JWTs): verify each request yourself. | ||
| const verifier: OAuthTokenVerifier = { | ||
| async verifyAccessToken(token): Promise<AuthInfo> { | ||
| const payload = await verifyJwtAgainstIssuer(token).catch(() => { | ||
| throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token'); | ||
| }); | ||
| return { token, clientId: payload.sub, scopes: payload.scopes, expiresAt: payload.exp }; | ||
| } | ||
| }; | ||
| const gate = requireBearerAuth({ verifier, requiredScopes: ['mcp'] }); | ||
|
|
||
| async function fetchHandler(request: Request): Promise<Response> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have a middleware pattern still? this one works well as a middleware, and is a bit awkward to do manually. |
||
| const auth = await gate(request); | ||
| if (auth instanceof Response) return auth; | ||
| return handler.fetch(request, { authInfo: auth }); | ||
| } | ||
| ``` | ||
|
|
||
| A request with a missing, malformed, or expired token gets the `401` challenge; a valid one reaches handlers with `ctx.http.authInfo` populated from your verifier. [Require authorization](./authorization.md) covers the rest of this style's surface: the challenge contents, publishing protected resource metadata that names your external issuer, and per-tool scopes. | ||
|
|
||
| ## Choosing a style | ||
|
|
||
| Style A fits when the AS can own your HTTP edge — a platform provider wrapping the worker, or an auth framework mounted in the same app. Style B fits when the AS is a remote system and your server is the edge: nothing fronts you, so you verify. They compose with the same application code either way — the factory receives `authInfo` identically, so switching styles later does not touch your tools. | ||
|
|
||
| ## Recap | ||
|
|
||
| - The SDK is Resource-Server-only by design: bring the Authorization Server from your platform, framework, or IdP. | ||
| - Style A: the AS fronts you and verifies; inject its identity with `handler.fetch(request, { authInfo })`. | ||
| - Style B: the AS is external; verify per request with `OAuthTokenVerifier` + `requireBearerAuth`. | ||
| - Embed what `AuthInfo` needs into the grant at consent time — fronting providers replay only what was stored. | ||
| - The `todos-server` example runs Style A live; the `oauth` example runs an in-process better-auth AS. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pcarleton this guide to using external-authorization-servers is probably worth checking if it matches your expectations?