Skip to content

Commit 69868af

Browse files
committed
feat: Add OAuth 2.0 authentication with session_id fallback
- Add @cloudflare/workers-oauth-provider for OAuth 2.0 support - Create src/index-oauth.ts as hybrid auth entry point - Create src/auth/oauth-handler.ts for Last.fm OAuth integration - Add manual /login flow for Claude Desktop/Code (no OAuth support) - Add OAuth discovery endpoints (RFC 9728 compliant) - Add OAUTH_KV namespace binding - Update tools with getMcpAuthContext() for OAuth - Update README with setup instructions for all clients - Update MCP-MODERNIZATION-PLAN.md with final implementation Authentication methods: - OAuth 2.0: Works with Windsurf and OAuth-compliant clients - Session ID: Manual login for Claude Desktop/Code via ?session_id= param
1 parent 85ea175 commit 69868af

File tree

10 files changed

+1493
-249
lines changed

10 files changed

+1493
-249
lines changed

MCP-MODERNIZATION-PLAN.md

Lines changed: 119 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,15 @@ Use this section to track progress across sessions:
436436
| 3. Authenticated Tools | ✅ Complete | 2024-12-10 | 12 authenticated tools with session context |
437437
| 4. Resources & Prompts | ✅ Complete | 2024-12-10 | 10 resources, 6 prompts migrated |
438438
| 5. Entry Point & Routing | ✅ Complete | 2024-12-10 | /mcp uses createMcpHandler, backward compat kept |
439-
| 6. Authentication | ❌ Blocked | 2024-12-10 | See "Multi-User Auth Challenge" below |
440-
| 7. Testing | ⬜ Not Started | | |
441-
| 8. Cleanup & Deploy | ⬜ Not Started | | |
439+
| 6. Authentication | ✅ Complete | 2024-12-10 | OAuth 2.0 + session_id fallback for Claude Desktop |
440+
| 7. Testing | ✅ Complete | 2024-12-10 | Tested with Windsurf (OAuth) and Claude Desktop (session_id) |
441+
| 8. Cleanup & Deploy | ✅ Complete | 2024-12-10 | Deployed to production |
442442

443443
Legend: ⬜ Not Started | 🟡 In Progress | ✅ Complete | ❌ Blocked
444444

445445
---
446446

447-
## Multi-User Auth Challenge (BLOCKING)
447+
## Multi-User Auth Challenge (RESOLVED)
448448

449449
### Problem Statement
450450

@@ -455,99 +455,132 @@ Claude Desktop's MCP connector does not persist session IDs across conversations
455455

456456
This means auth stored under `session:{uuid1}` is lost when the next conversation uses `session:{uuid2}`.
457457

458-
### Current State (as of 2024-12-10)
458+
### Solution: Hybrid Authentication (Completed 2024-12-10)
459459

460-
- ✅ SDK integration working (`/mcp` endpoint uses `createMcpHandler`)
461-
- ✅ Session ID extraction from headers and URL params
462-
- ✅ Auth stored in KV per session ID
463-
- ✅ Login flow works and stores auth correctly
464-
- ❌ Auth does not persist across Claude Desktop conversations
465-
- ⚠️ Temporary global fallback implemented (NOT safe for multi-user)
466-
467-
### Options for Multi-User Production
468-
469-
#### Option 1: OAuth 2.0 with PKCE (Recommended)
470-
**Status**: Not implemented
471-
**Effort**: High
472-
**Pros**: MCP spec compliant, industry standard, secure
473-
**Cons**: Complex implementation, requires OAuth provider setup
474-
475-
The MCP spec recommends OAuth 2.0 for remote server authentication. Cloudflare Agents SDK supports this via:
476-
- `@cloudflare/workers-oauth-provider` package
477-
- `OAuthProvider` class with `createMcpHandler`
478-
- Tokens stored client-side, persist across sessions
479-
480-
Implementation steps:
481-
1. Set up OAuth provider (Cloudflare Access or custom)
482-
2. Implement authorization endpoint
483-
3. Implement token endpoint
484-
4. Use `getMcpAuthContext()` in tools
485-
5. Map OAuth identity to Last.fm session
486-
487-
References:
488-
- [MCP Authorization Spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
489-
- [Cloudflare MCP Authorization](https://developers.cloudflare.com/agents/model-context-protocol/authorization/)
490-
491-
#### Option 2: Cloudflare Access Integration
492-
**Status**: Not implemented
493-
**Effort**: Medium
494-
**Pros**: Handles auth at edge, enterprise-grade
495-
**Cons**: Requires Cloudflare Access subscription, adds dependency
496-
497-
Use Cloudflare Access as OAuth provider:
498-
1. Configure Access application for the MCP server
499-
2. Access handles authentication before request reaches worker
500-
3. Worker receives authenticated user identity in headers
501-
4. Map Access identity to Last.fm session
502-
503-
Reference: [Secure MCP with Access](https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/saas-mcp/)
504-
505-
#### Option 3: User-Configured Session IDs
506-
**Status**: Implemented (workaround)
507-
**Effort**: Low
508-
**Pros**: Works now, no server changes needed
509-
**Cons**: Poor UX, requires user to configure URL
510-
511-
Users add a fixed session ID to their connector URL:
512-
```
513-
https://lastfm-mcp-prod.rian-db8.workers.dev/mcp?session_id=USER-SPECIFIC-UUID
514-
```
460+
We implemented **two authentication methods** to support different MCP clients:
515461

516-
This is a valid workaround but not ideal for a public service.
462+
1. **OAuth 2.0** - For clients that support it (Windsurf, future Claude updates)
463+
2. **Session ID URL parameter** - For clients that don't support OAuth (Claude Desktop)
517464

518-
#### Option 4: Cookie-Based Auth with SameSite=None
519-
**Status**: Partially implemented
520-
**Effort**: Low
521-
**Pros**: Standard web auth pattern
522-
**Cons**: MCP clients may not support cookies, CORS complexity
465+
**New Files Created:**
466+
- `src/index-oauth.ts` - Hybrid auth entry point (OAuth + session_id fallback)
467+
- `src/auth/oauth-handler.ts` - Last.fm OAuth integration + manual login flow
468+
- `src/mcp/tools/authenticated.ts` - Added `registerAuthenticatedToolsWithOAuth()` function
523469

524-
Cookies are set on auth callback but Claude's connector likely doesn't persist them.
470+
**Key Changes:**
471+
- Added `@cloudflare/workers-oauth-provider` dependency
472+
- Added `OAUTH_KV` namespace binding to `wrangler.toml`
473+
- OAuth tools use `getMcpAuthContext()` for auth from OAuth tokens
474+
- Session-based tools use KV lookup via `session_id` query parameter
475+
- Manual `/login` endpoint for Claude Desktop users
525476

526-
### Recommended Path Forward
477+
### Current State (as of 2024-12-10)
527478

528-
For a **public multi-user service**, implement **Option 1 (OAuth 2.0)** with this approach:
479+
**OAuth 2.0 (RFC 9728 compliant):**
480+
- ✅ OAuth discovery (`/.well-known/oauth-authorization-server`)
481+
- ✅ Protected resource metadata (`/.well-known/oauth-protected-resource`)
482+
- ✅ Client registration endpoint (`/oauth/register`)
483+
- ✅ Authorization endpoint (`/authorize` → Last.fm)
484+
- ✅ Token exchange endpoint (`/oauth/token`)
485+
- ✅ Last.fm callback (`/lastfm-callback`)
486+
- ✅ Proper `WWW-Authenticate` header with `resource_metadata` URL
487+
- ✅ Works with Windsurf and OAuth-compliant MCP clients
529488

530-
1. **Phase 1**: Keep current implementation for local/personal use
531-
2. **Phase 2**: Add OAuth 2.0 support alongside current auth
532-
3. **Phase 3**: Deprecate session-based auth, require OAuth
489+
**Session ID Fallback (for Claude Desktop):**
490+
- ✅ Manual login endpoint (`/login`)
491+
- ✅ Session stored in KV with 30-day TTL
492+
- ✅ Session ID passed via URL query parameter
493+
- ✅ Works with Claude Desktop via `?session_id=` parameter
533494

534-
OAuth implementation outline:
535-
```typescript
536-
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
537-
import { createMcpHandler, getMcpAuthContext } from "agents/mcp";
495+
### Production Architecture
538496

539-
// In tool handler:
540-
const authContext = getMcpAuthContext();
541-
const userId = authContext.props.sub; // From OAuth token
542-
const lastfmSession = await lookupLastfmSession(userId);
543-
```
497+
**Entry Point:** `src/index-oauth.ts` (hybrid auth)
498+
**Deploy Command:** `npm run deploy:prod`
499+
500+
#### Authentication Flow Diagram
544501

545-
### Temporary Workaround (Current)
502+
```
503+
┌─────────────────────────────────────────────────────────────────┐
504+
│ MCP Client Request │
505+
│ POST /mcp │
506+
└─────────────────────────────┬───────────────────────────────────┘
507+
508+
509+
┌──────────────────────┐
510+
│ Has session_id param? │
511+
└──────────────────────┘
512+
│ │
513+
YES NO
514+
│ │
515+
▼ ▼
516+
┌─────────────────┐ ┌─────────────────────┐
517+
│ Lookup session │ │ OAuthProvider │
518+
│ from KV │ │ checks Bearer token │
519+
└────────┬────────┘ └──────────┬──────────┘
520+
│ │
521+
┌─────┴─────┐ ┌─────┴─────┐
522+
│ Valid? │ │ Valid? │
523+
└───────────┘ └───────────┘
524+
│ │ │ │
525+
YES NO YES NO
526+
│ │ │ │
527+
▼ ▼ ▼ ▼
528+
┌──────┐ ┌──────────┐ ┌──────┐ ┌──────────────┐
529+
│ MCP │ │ 401 + │ │ MCP │ │ 401 + │
530+
│Server│ │ login URL│ │Server│ │ WWW-Auth │
531+
└──────┘ └──────────┘ └──────┘ │ resource_ │
532+
│ metadata │
533+
└──────────────┘
534+
```
546535

547-
Global auth fallback is implemented but **NOT SAFE for multi-user**:
548-
- Last authenticated user becomes the "default" for everyone
549-
- Acceptable for single-user testing only
550-
- Must be replaced with proper OAuth before public launch
536+
#### Client Compatibility
537+
538+
| Client | Auth Method | Status |
539+
|--------|-------------|--------|
540+
| Windsurf | OAuth 2.0 | ✅ Working |
541+
| Claude Desktop | session_id URL param | ✅ Working (manual setup) |
542+
| Claude Code | session_id URL param | ✅ Working (manual setup) |
543+
| MCP Inspector | OAuth 2.0 | ✅ Should work |
544+
| Custom clients | Either | ✅ Both supported |
545+
546+
#### OAuth 2.0 Flow (Windsurf, etc.)
547+
548+
1. Client requests `/mcp` → gets 401 with `WWW-Authenticate: Bearer resource_metadata="..."`
549+
2. Client fetches `/.well-known/oauth-protected-resource`
550+
3. Client fetches `/.well-known/oauth-authorization-server`
551+
4. Client registers via `/oauth/register`
552+
5. Client redirects user to `/authorize`
553+
6. Server redirects to Last.fm auth
554+
7. Last.fm redirects to `/lastfm-callback`
555+
8. Server completes OAuth, issues token
556+
9. Client accesses `/mcp` with Bearer token
557+
558+
#### Session ID Flow (Claude Desktop)
559+
560+
1. User visits `https://lastfm-mcp-prod.rian-db8.workers.dev/login`
561+
2. Redirects to Last.fm authentication
562+
3. Last.fm redirects to `/callback`
563+
4. Server stores session in KV, shows session ID
564+
5. User adds `?session_id=XXX` to their MCP config URL
565+
6. All requests to `/mcp?session_id=XXX` use stored session
566+
567+
#### Endpoint Reference
568+
569+
| Endpoint | Method | Purpose |
570+
|----------|--------|--------|
571+
| `/` | GET | API info JSON |
572+
| `/mcp` | POST | MCP JSON-RPC endpoint |
573+
| `/mcp?session_id=XXX` | POST | MCP with session auth |
574+
| `/login` | GET | Manual login (Claude Desktop) |
575+
| `/callback` | GET | Manual login callback |
576+
| `/authorize` | GET | OAuth authorization |
577+
| `/oauth/token` | POST | OAuth token exchange |
578+
| `/oauth/register` | POST | OAuth client registration |
579+
| `/lastfm-callback` | GET | OAuth Last.fm callback |
580+
| `/.well-known/oauth-authorization-server` | GET | OAuth server metadata |
581+
| `/.well-known/oauth-protected-resource` | GET | OAuth resource metadata |
582+
| `/.well-known/mcp.json` | GET | MCP server discovery |
583+
| `/health` | GET | Health check |
551584

552585
---
553586

0 commit comments

Comments
 (0)