# MCP Server Template: Multi-Layer Authentication Architecture
A design document for the mcp-server-template-cloudflare authentication system, properly separating **MCP client authentication** from **backend API authentication**.
## The Two Authentication Layers
MCP servers have **two distinct authentication concerns** that are often conflated:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ MCP SERVER │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ LAYER 1: MCP Client Authentication │ │
│ │ "Who is connecting to this MCP server?" │ │
│ │ │ │
│ │ • Claude.ai, ElevenLabs, CLI tools connecting to YOUR server │ │
│ │ • Handled by @cloudflare/workers-oauth-provider │ │
│ │ • Issues MCP access tokens to clients │ │
│ │ • Identity comes from: Google, Microsoft, GitHub (as OIDC provider) │ │
│ │ • OR: Bearer token (bypasses OAuth entirely) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ LAYER 2: Backend API Authentication │ │
│ │ "How does the MCP server access upstream APIs?" │ │
│ │ │ │
│ │ • Your server calling Google, Microsoft, Xero, Rocket.net APIs │ │
│ │ • INDEPENDENT of Layer 1 - can use different providers │ │
│ │ • Options: │ │
│ │ - User OAuth tokens (user authorises their account) │ │
│ │ - User-stored credentials (encrypted in D1) │ │
│ │ - Server API keys (single credential for all users) │ │
│ │ - None (public APIs) │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ UPSTREAM APIs │ │
│ │ Google, Xero, │ │
│ │ Rocket.net, etc. │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Layer 1: MCP Client Authentication
This layer answers: **"Who is connecting to this MCP server?"**
The MCP specification uses [OAuth 2.1 for authorization](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/). The `@cloudflare/workers-oauth-provider` library implements the OAuth Authorization Server, allowing MCP clients (like Claude.ai) to connect.
### Layer 1 Options
| Option | How It Works | Use Cases |
|--------|--------------|-----------|
| **Identity Provider OAuth** | User authenticates via Google/Microsoft/GitHub. MCP server issues its own token to the client. | Claude.ai, web-based MCP clients |
| **Bearer Token** | Static token in `Authorization` header. Bypasses OAuth entirely. | ElevenLabs, CLI tools, automation |
| **None** | No authentication required. Anyone can connect. | Public demo servers (rare) |
### Identity Providers for Layer 1
When using OAuth for Layer 1, you need an **identity provider** to authenticate users. The identity provider tells you WHO the user is.
| Provider | Endpoints | Notes |
|----------|-----------|-------|
| **Google** (current) | accounts.google.com | Most common for consumer apps |
| **Microsoft Entra** | login.microsoftonline.com | Enterprise, Microsoft 365 orgs |
| **GitHub** | github.com/login/oauth | Developer tools |
| **Auth0/Stytch/WorkOS** | Varies | Enterprise SSO, custom identity |
**Key insight**: Layer 1 only needs **identity** (email, name, user ID). You don't necessarily need API access tokens from the identity provider.
### Current Implementation (Google Identity)
```typescript
// The OAuthProvider handles MCP protocol OAuth
const oauthProvider = new OAuthProvider({
apiHandlers: { '/sse': sseHandler, '/mcp': mcpHandler },
authorizeEndpoint: '/authorize',
tokenEndpoint: '/token',
clientRegistrationEndpoint: '/register',
// GoogleHandler provides identity via Google OAuth
defaultHandler: GoogleHandler,
});
```
The `GoogleHandler` redirects to Google, gets user info, and passes it to the MCP session via `props`:
```typescript
// After Google OAuth callback, user identity is available
const props = {
id: googleUser.id, // Google user ID
email: googleUser.email, // User's email
name: googleUser.name, // Display name
picture: googleUser.picture,
// These are OPTIONAL - only needed if Layer 2 uses Google APIs:
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
tokenExpiresAt: tokens.expiresAt,
};
```
---
## Layer 2: Backend API Authentication
This layer answers: **"How does the MCP server access upstream APIs?"**
This is **completely independent** of Layer 1. You could authenticate users via Google (Layer 1) but call Rocket.net APIs with user-stored credentials (Layer 2).
### Layer 2 Patterns
| Pattern | Description | Token Ownership | Examples |
|---------|-------------|-----------------|----------|
| **User OAuth** | User authorises the server to access their account via OAuth | Per-user tokens from OAuth flow | Google Workspace, Microsoft 365, Xero |
| **User Credentials** | User provides API key, password, or token - stored encrypted | Per-user, encrypted in D1 | Rocket.net, ServiceM8, AWS keys |
| **Server OAuth** | Admin authorises a shared account, all users access it | Server-level OAuth tokens | Shared calendar, company Xero, team Slack |
| **Server API Key** | Server has single credential for all users | Server-level secret | Synergy Wholesale (reseller), rate-limited public APIs |
| **None** | No authentication needed | N/A | Postcodes lookup, ABN lookup, public APIs |
### Pattern Details
#### Pattern A: User OAuth (Same Provider)
User authenticates AND authorises API access in a single OAuth flow.
```
Layer 1: Google OAuth (identity)
Layer 2: Google OAuth (API access) ← Same flow, same tokens
```
**Use when**: Your MCP server calls APIs from the same provider as identity.
**Examples**:
- Google Tasks MCP (Google identity + Google Tasks API)
- Microsoft 365 MCP (Microsoft identity + Microsoft Graph API)
- Xero MCP (Xero identity + Xero API)
**Token flow**:
```typescript
// Single OAuth flow provides both identity AND API tokens
props = {
// Identity (Layer 1)
email: 'user@example.com',
name: 'User Name',
// API Access (Layer 2)
accessToken: 'ya29.xxx', // Used for API calls
refreshToken: '1//xxx', // For refreshing
tokenExpiresAt: 1704067200000,
};
```
#### Pattern B: User OAuth (Cross-Provider)
User authenticates with one provider, authorises API access with another.
```
Layer 1: Google OAuth (identity)
Layer 2: Xero OAuth (API access) ← Different provider!
```
**Use when**: You want a consistent identity provider but need access to different services.
**Examples**:
- Google identity + Xero accounting
- Microsoft identity + Salesforce CRM
**Token flow** (requires two separate OAuth flows):
```typescript
// Step 1: User authenticates with Google (Layer 1)
props = {
email: 'user@example.com',
name: 'User Name',
};
// Step 2: User clicks "Connect Xero" and authorises (Layer 2)
// Xero tokens stored separately in D1, keyed by user ID
const xeroTokens = await db.prepare(
'SELECT * FROM api_tokens WHERE user_id = ? AND provider = ?'
).bind(props.id, 'xero').first();
```
#### Pattern C: User Credential Storage
User provides their own API credentials, stored encrypted per-user.
```
Layer 1: Google OAuth (identity)
Layer 2: Encrypted credentials in D1 (per-user)
```
**Use when**: The upstream API uses credentials other than OAuth - API keys, username/password, tokens, etc.
**Credential types supported**:
| Type | Fields | Examples |
|------|--------|----------|
| API Key | `api_key` | ServiceM8, many SaaS APIs |
| API Key + Secret | `api_key`, `api_secret` | AWS, some payment APIs |
| Username + Password | `username`, `password` | Rocket.net, cPanel, WHM |
| Bearer Token | `token` | Personal access tokens |
| Custom | Any fields | Reseller IDs, account numbers |
**Examples**:
- Rocket.net (username/password for each user's account)
- ServiceM8 (API key per user)
- AWS (access key + secret key per user)
- cPanel/WHM (username/password per server)
- GitHub (personal access token)
**Implementation (API Key)**:
```typescript
// User provides API key via tool
this.server.tool('credential_add', 'Store your API key', {
api_key: z.string().describe('Your API key from the service dashboard'),
label: z.string().optional().describe('Label for this credential'),
}, async ({ api_key, label }) => {
// Encrypt and store, keyed by user ID
const encrypted = await encrypt(api_key, env.CREDENTIAL_ENCRYPTION_KEY);
await db.prepare(
'INSERT INTO credentials (user_id, credential_type, value_encrypted, label) VALUES (?, ?, ?, ?)'
).bind(props.id, 'api_key', encrypted, label).run();
});
// When making API calls, decrypt and use
const creds = await getCredentials(props.id);
const response = await fetch(apiUrl, {
headers: { 'Authorization': `Bearer ${creds.api_key}` }
});
```
**Implementation (Username + Password)**:
```typescript
// User provides credentials via tool
this.server.tool('credential_add', 'Store API credentials', {
username: z.string(),
password: z.string(),
label: z.string().optional(),
}, async ({ username, password, label }) => {
// Encrypt password (username can be plain or encrypted too)
const encrypted = await encrypt(password, env.CREDENTIAL_ENCRYPTION_KEY);
await db.prepare(
'INSERT INTO credentials (user_id, username, password_encrypted, label) VALUES (?, ?, ?, ?)'
).bind(props.id, username, encrypted, label).run();
});
// When making API calls, decrypt and use
const creds = await getCredentials(props.id);
const response = await rocketnetAPI.login(creds.username, creds.password);
```
#### Pattern D: Server OAuth (Admin-Provided)
Admin authorises a shared account; all MCP users access that same account.
```
Layer 1: Google OAuth (identity) - just to know who's using it
Layer 2: Admin-connected OAuth tokens (stored server-wide)
```
**Use when**: You want all users to access a shared resource that requires OAuth.
**Examples**:
- **Shared team calendar** - Admin connects their Google Calendar, all team members can view/add events
- **Company Xero** - Admin connects the company Xero org, all staff can create invoices
- **Team Slack** - Admin connects Slack workspace, MCP can post to channels
- **Shared Google Drive** - Admin connects a folder, all users can access documents
- **Company HubSpot** - Admin connects CRM, all sales team can access contacts
**Key differences from User OAuth**:
| Aspect | User OAuth | Server OAuth |
|--------|------------|--------------|
| Who authorises | Each user | Admin only |
| Token storage | Per-user in D1 | Server-wide in KV |
| Access scope | User's own data | Shared account |
| Connection UI | User sees "Connect" | Admin dashboard only |
**Implementation**:
```typescript
// Admin connects via /admin/connect/xero
// Tokens stored at server level (not per-user)
app.get('/admin/connect/xero', requireAdmin, async (c) => {
const state = crypto.randomUUID();
await c.env.OAUTH_KV.put(`xero_state:${state}`, 'pending', { expirationTtl: 600 });
const authUrl = new URL('https://login.xero.com/identity/connect/authorize');
authUrl.searchParams.set('client_id', c.env.XERO_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', `${baseUrl}/admin/callback/xero`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email accounting.transactions');
authUrl.searchParams.set('state', state);
return c.redirect(authUrl.toString());
});
// Callback stores tokens at SERVER level
app.get('/admin/callback/xero', requireAdmin, async (c) => {
const code = c.req.query('code');
const tokens = await exchangeXeroCode(code, c.env);
// Store server-wide (no user ID key)
await c.env.OAUTH_KV.put('server:xero:tokens', JSON.stringify({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
connectedBy: adminUser.email, // Audit trail
connectedAt: Date.now(),
}));
return c.redirect('/admin?connected=xero');
});
// Any user's tool call uses the server tokens
this.server.tool('xero_list_invoices', 'List invoices from connected Xero', {}, async () => {
const serverTokens = await getServerXeroTokens(env);
if (!serverTokens) {
return { content: [{ type: 'text', text: 'Xero not connected. Ask an admin to connect.' }], isError: true };
}
// Refresh if needed (server-level refresh)
const tokens = await ensureValidServerTokens(env, 'xero', serverTokens);
const invoices = await xeroAPI.getInvoices(tokens.accessToken);
return { content: [{ type: 'text', text: JSON.stringify(invoices) }] };
});
```
**Admin dashboard UI**:
```
┌─────────────────────────────────────────────────────────────┐
│ Connected Services │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ Xero (Company Account) │
│ Connected by: admin@company.com on 2026-01-02 │
│ [Disconnect] │
│ │
│ ❌ Google Calendar (Shared) │
│ Not connected │
│ [Connect Google Calendar] │
│ │
│ ❌ Slack (Team Workspace) │
│ Not connected │
│ [Connect Slack] │
│ │
└─────────────────────────────────────────────────────────────┘
```
**Security considerations**:
- Only admins can connect/disconnect
- All users get same level of access (no per-user scopes)
- Audit log of who connected and when
- Consider adding user allowlist for sensitive operations
- Token refresh happens at server level (background job or on-demand)
#### Pattern E: Server API Key
Single set of credentials for all users (reseller model or shared access).
```
Layer 1: Google OAuth (identity) or Bearer token
Layer 2: Server secret (env var)
```
**Use when**: You have reseller/partner credentials, or want to control access centrally.
**Examples**:
- Synergy Wholesale (reseller API credentials)
- OpenAI API (your API key, users don't need their own)
- Rate-limited public APIs (your key for higher limits)
**Implementation**:
```typescript
// Credentials in env vars
const apiClient = new SynergyWholesaleAPI({
resellerId: env.SYNERGY_RESELLER_ID,
apiKey: env.SYNERGY_API_KEY,
});
// All users share the same API access
const domains = await apiClient.listDomains();
```
#### Pattern F: No Backend Auth
No authentication needed for upstream API.
```
Layer 1: Google OAuth (identity) or Bearer token or None
Layer 2: None
```
**Use when**: The API is public or your server IS the data source.
**Examples**:
- Postcodes MCP (queries local D1 database)
- ABN Lookup (public government API)
- Weather API (public, no auth)
---
## Configuration Architecture
### Environment Variables
```jsonc
// wrangler.jsonc
{
"vars": {
// === LAYER 1: MCP Client Auth ===
"IDENTITY_PROVIDER": "google", // "google" | "microsoft" | "github" | "none"
// === LAYER 2: Backend API Auth ===
"BACKEND_AUTH_TYPE": "user-oauth-same", // See options below
// Options:
// - "user-oauth-same" : Same OAuth as identity (Google API, Microsoft Graph)
// - "user-oauth-cross" : Different OAuth provider (needs BACKEND_OAUTH_PROVIDER)
// - "user-credentials" : User stores their credentials (encrypted D1)
// - "server-oauth" : Admin connects shared account (company Xero, team calendar)
// - "server-key" : Server-level API key
// - "none" : No backend auth needed
// For "user-oauth-cross" or "server-oauth":
"BACKEND_OAUTH_PROVIDER": "", // "xero" | "google" | "slack" | "hubspot" | "custom"
// OAuth scopes (for user-oauth patterns)
"OAUTH_SCOPES": "openid email profile", // Identity scopes
"API_SCOPES": "https://www.googleapis.com/auth/tasks", // API scopes (if applicable)
}
}
```
### Secrets
```bash
# === LAYER 1: Identity Provider ===
# Google (if IDENTITY_PROVIDER=google)
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
# Microsoft (if IDENTITY_PROVIDER=microsoft)
npx wrangler secret put MICROSOFT_CLIENT_ID
npx wrangler secret put MICROSOFT_CLIENT_SECRET
npx wrangler secret put MICROSOFT_TENANT_ID # Optional, 'common' for multi-tenant
# GitHub (if IDENTITY_PROVIDER=github)
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
# === LAYER 2: Backend API Auth ===
# User credentials encryption (if BACKEND_AUTH_TYPE=user-credentials)
npx wrangler secret put CREDENTIAL_ENCRYPTION_KEY
# Server API key (if BACKEND_AUTH_TYPE=server-key)
npx wrangler secret put API_KEY
# Or provider-specific:
npx wrangler secret put SYNERGY_RESELLER_ID
npx wrangler secret put SYNERGY_API_KEY
# Cross-provider OAuth (if BACKEND_AUTH_TYPE=user-oauth-cross)
npx wrangler secret put XERO_CLIENT_ID
npx wrangler secret put XERO_CLIENT_SECRET
# === Common ===
npx wrangler secret put COOKIE_ENCRYPTION_KEY
npx wrangler secret put AUTH_TOKEN # Bearer token for programmatic access
```
---
## Real-World Examples
### Example 1: Google Tasks MCP (Current Template)
```
Layer 1: Google OAuth (identity)
Layer 2: Google OAuth (API access) - SAME PROVIDER
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "google",
"BACKEND_AUTH_TYPE": "user-oauth-same",
"OAUTH_SCOPES": "openid email profile",
"API_SCOPES": "https://www.googleapis.com/auth/tasks"
}
}
```
Single OAuth flow handles both layers. `props` contains identity + API tokens.
### Example 2: Rocket.net MCP
```
Layer 1: Google OAuth (identity)
Layer 2: User-stored credentials (encrypted)
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "google",
"BACKEND_AUTH_TYPE": "user-credentials",
"OAUTH_SCOPES": "openid email profile"
}
}
```
Secrets:
```bash
npx wrangler secret put CREDENTIAL_ENCRYPTION_KEY
```
User stores Rocket.net username/password via `credential_add` tool. Credentials encrypted in D1, keyed by Google user ID.
### Example 3: Postcodes MCP
```
Layer 1: Google OAuth (identity) OR Bearer token
Layer 2: None (local D1 database)
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "google",
"BACKEND_AUTH_TYPE": "none",
"OAUTH_SCOPES": "openid email profile"
}
}
```
No backend auth needed - the MCP server IS the data source.
### Example 4: Microsoft 365 MCP (Proposed)
```
Layer 1: Microsoft Entra (identity)
Layer 2: Microsoft Graph (API access) - SAME PROVIDER
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "microsoft",
"BACKEND_AUTH_TYPE": "user-oauth-same",
"OAUTH_SCOPES": "openid email profile User.Read",
"API_SCOPES": "Mail.Read Calendars.ReadWrite Files.Read.All"
}
}
```
### Example 5: Shared Team Calendar MCP (Server OAuth)
```
Layer 1: Google OAuth (identity) - know who's using it
Layer 2: Server OAuth (admin-connected Google Calendar)
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "google",
"BACKEND_AUTH_TYPE": "server-oauth",
"BACKEND_OAUTH_PROVIDER": "google",
"OAUTH_SCOPES": "openid email profile",
"API_SCOPES": "https://www.googleapis.com/auth/calendar"
}
}
```
Admin connects the shared calendar via `/admin/connect/google`. All users can then view and add events to that calendar. The calendar owner's tokens are stored server-wide.
### Example 6: Company Xero MCP (Server OAuth)
```
Layer 1: Google OAuth (identity) - company SSO
Layer 2: Server OAuth (admin-connected Xero org)
```
Config:
```jsonc
{
"vars": {
"IDENTITY_PROVIDER": "google",
"BACKEND_AUTH_TYPE": "server-oauth",
"BACKEND_OAUTH_PROVIDER": "xero",
"OAUTH_SCOPES": "openid email profile"
}
}
```
Admin connects the company Xero account. All authorised employees can create invoices, view transactions, etc. using the shared Xero connection.
### Example 7: Multi-Service MCP (Proposed)
```
Layer 1: Google OAuth (identity)
Layer 2: Multiple backends
- Xero (user-oauth-cross)
- Rocket.net (user-credentials)
```
This requires supporting multiple Layer 2 backends in a single server:
```typescript
// Different tools use different backends
this.server.tool('xero_invoices', ..., async () => {
const xeroTokens = await getXeroTokens(props.id);
// Use Xero API
});
this.server.tool('rocketnet_sites', ..., async () => {
const creds = await getRocketnetCredentials(props.id);
// Use Rocket.net API
});
```
---
## Directory Structure
### Current Implementation (Phase 1 Complete)
```
src/
├── index.ts # Main entry, OAuth provider setup
├── types.ts # Shared TypeScript interfaces
│
├── auth/ # ✅ IMPLEMENTED: Unified auth module
│ ├── index.ts # Auth exports
│ ├── types.ts # Shared auth types (IdentityProviderType, BackendAuthType)
│ │
│ ├── identity/ # ✅ LAYER 1: Identity providers
│ │ ├── index.ts # Identity provider exports
│ │ ├── types.ts # UserIdentity, IdentityTokens, IdentityProvider interfaces
│ │ └── google.ts # ✅ Google OAuth implementation
│ │
│ └── backend/ # ✅ LAYER 2: Backend API auth (interfaces only)
│ ├── index.ts # Backend auth exports
│ └── types.ts # BackendCredentials, StoredCredential interfaces
│
├── oauth/ # MCP OAuth protocol handlers
│ ├── google-handler.ts # OAuth routes (slimmed, imports from auth/identity)
│ ├── utils.ts # Re-exports from auth/identity/google (deprecated)
│ └── workers-oauth-utils.ts # CSRF, state, approval dialog utilities
│
├── pages/
│ └── homepage.ts # ✅ Server homepage (extracted, XSS-safe)
│
├── admin/ # Admin dashboard
├── lib/ # AI, memory, agent modules
├── tools/ # MCP tools
├── resources/ # MCP resources
└── prompts/ # MCP prompts
```
### Future Additions (Phases 2-5)
```
src/auth/
├── identity/
│ ├── microsoft.ts # Phase 2: Microsoft Entra
│ ├── github.ts # Phase 2: GitHub OAuth
│ └── none.ts # Phase 2: No identity (Bearer token only)
│
└── backend/
├── user-oauth.ts # Phase 3: User OAuth tokens
├── user-credentials.ts # Phase 3: User-stored credentials (encrypted D1)
├── server-oauth.ts # Phase 3: Admin-connected OAuth
├── server-key.ts # Phase 3: Server-level API keys
└── none.ts # Phase 3: No backend auth
src/credentials/ # Phase 3: For user-credentials pattern
├── index.ts
├── encryption.ts
└── manager.ts
```
---
## Interface Definitions
### Layer 1: Identity Provider
```typescript
// src/auth/identity/base.ts
export interface UserIdentity {
id: string; // Provider-specific user ID
email: string; // User's email
name?: string; // Display name
picture?: string; // Profile picture URL
provider: string; // "google" | "microsoft" | "github"
raw?: unknown; // Full provider response
}
export interface IdentityTokens {
accessToken: string;
refreshToken?: string;
expiresAt?: number;
idToken?: string; // OIDC ID token (for identity verification)
}
export abstract class BaseIdentityProvider {
abstract get providerName(): string;
abstract get authorizationEndpoint(): string;
abstract get tokenEndpoint(): string;
abstract get userInfoEndpoint(): string;
// OAuth flow
abstract buildAuthUrl(state: string, redirectUri: string): string;
abstract exchangeCode(code: string, redirectUri: string): Promise<IdentityTokens>;
abstract getUserInfo(accessToken: string): Promise<UserIdentity>;
// Optional: Refresh (only needed if tokens used for Layer 2)
async refreshTokens?(refreshToken: string): Promise<IdentityTokens>;
}
```
### Layer 2: Backend Auth
```typescript
// src/auth/backend/base.ts
export interface BackendCredentials {
type: 'oauth' | 'api-key' | 'username-password' | 'none';
// For OAuth
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
// For API key
apiKey?: string;
// For username/password
username?: string;
password?: string;
// Provider-specific extras
tenantId?: string; // Xero, QuickBooks
realmId?: string; // QuickBooks
}
export abstract class BaseBackendAuth {
abstract get authType(): string;
// Get credentials for API calls
abstract getCredentials(userId: string): Promise<BackendCredentials>;
// Store credentials (for user-provided patterns)
async storeCredentials?(userId: string, credentials: BackendCredentials): Promise<void>;
// Refresh (for OAuth patterns)
async refreshCredentials?(userId: string, refreshToken: string): Promise<BackendCredentials>;
// Check if credentials exist/are valid
abstract hasValidCredentials(userId: string): Promise<boolean>;
}
```
### Combined Props
```typescript
// src/types.ts
export interface Props {
// Layer 1: Identity (always present after auth)
id: string;
email: string;
name?: string;
picture?: string;
identityProvider: string;
// Layer 2: Backend auth (optional, depends on BACKEND_AUTH_TYPE)
// Only populated if BACKEND_AUTH_TYPE = "user-oauth-same"
accessToken?: string;
refreshToken?: string;
tokenExpiresAt?: number;
// For cross-provider OAuth, tokens stored in D1 separately
// For user-credentials, credentials stored in D1 separately
// For server-oauth, tokens stored server-wide in KV (not per-user)
// For server-key, no per-user data needed
}
```
---
## Migration Path
### Phase 1: Refactor Current Google OAuth ✅ COMPLETE
**Completed**: 2026-01-02
1. ✅ Created `src/auth/` module structure with identity and backend submodules
2. ✅ Added type interfaces (`IdentityProvider`, `UserIdentity`, `IdentityTokens`, `BackendCredentials`)
3. ✅ Extracted Google OAuth logic to `src/auth/identity/google.ts`
4. ✅ Extracted homepage to `src/pages/homepage.ts` with XSS-safe `HomepageConfig`
5. ✅ Maintained backward compatibility via re-exports in `src/oauth/utils.ts`
6. ✅ Updated `src/index.ts` imports
**Files created**:
- `src/auth/index.ts`, `src/auth/types.ts`
- `src/auth/identity/index.ts`, `src/auth/identity/types.ts`, `src/auth/identity/google.ts`
- `src/auth/backend/index.ts`, `src/auth/backend/types.ts`
- `src/pages/homepage.ts`
**Files modified**:
- `src/oauth/google-handler.ts` (slimmed from 744 to 349 lines)
- `src/oauth/utils.ts` (now just re-exports)
- `src/index.ts` (updated import path)
### Phase 2: Add Identity Provider Abstraction
1. Create identity provider factory
2. Add Microsoft Entra provider
3. Add GitHub provider
4. Configuration via `IDENTITY_PROVIDER` env var
### Phase 3: Separate Backend Auth
1. Create `src/auth/backend/` module
2. Extract `authorizedFetch` logic to backend auth
3. Support `user-oauth-same` (current behavior)
4. Support `user-credentials` (Rocket.net pattern)
5. Support `server-key` (simple API key pattern)
6. Support `none` (no backend auth)
### Phase 4: Cross-Provider OAuth
1. Add separate OAuth flow for backend providers
2. Store backend tokens in D1 (separate from identity)
3. Add "Connect [Provider]" flow in admin dashboard
4. Support multiple backends per server
### Phase 5: Documentation & Templates
1. Update CUSTOMIZATION.md with new patterns
2. Create example servers for each pattern
3. Add setup wizard for common configurations
---
## Security Considerations
### Layer 1 Security
- **State parameter**: Prevent CSRF on OAuth flow
- **PKCE**: Use for all providers that support it
- **Token validation**: Verify ID tokens cryptographically
- **Session binding**: Tie OAuth state to browser session
### Layer 2 Security
- **Encryption at rest**: AES-GCM for stored credentials
- **Unique IVs**: Never reuse initialization vectors
- **Key management**: Encryption keys as Wrangler secrets
- **Token refresh**: Proactive refresh before expiry
- **Scope minimization**: Request only needed OAuth scopes
- **Credential isolation**: Users can only access their own credentials
### Cross-Cutting
- **Timing-safe comparison**: For Bearer token validation
- **Rate limiting**: Protect auth endpoints
- **Audit logging**: Track credential access (optional)
---
## Summary: Choosing Your Pattern
| Scenario | Layer 1 | Layer 2 | Example |
|----------|---------|---------|---------|
| Google Workspace tools | Google OAuth | User OAuth (same) | google-tasks-mcp |
| Microsoft 365 tools | Microsoft OAuth | User OAuth (same) | microsoft-365-mcp |
| WordPress hosting tools | Google OAuth | User credentials | rocketnet-mcp |
| **Shared team calendar** | Google OAuth | **Server OAuth** | team-calendar-mcp |
| **Company accounting** | Google OAuth | **Server OAuth** | company-xero-mcp |
| **Team Slack integration** | Google OAuth | **Server OAuth** | team-slack-mcp |
| Reseller/partner API | Google OAuth or Bearer | Server API key | synergy-wholesale-mcp |
| Local data/public API | Google OAuth or Bearer | None | postcodes-mcp |
| Enterprise + Xero | Microsoft OAuth | User OAuth (cross) | enterprise-xero-mcp |
---
## References
- [MCP Authorization Spec](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/)
- [Cloudflare Workers OAuth Provider](https://github.com/cloudflare/workers-oauth-provider)
- [Cloudflare MCP Authorization Docs](https://developers.cloudflare.com/agents/model-context-protocol/authorization/)
- [Google OAuth 2.0](https://developers.google.com/identity/protocols/oauth2)
- [Microsoft Identity Platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/)
- [GitHub OAuth](https://docs.github.com/en/developers/apps/building-oauth-apps)
- [Xero OAuth 2.0](https://developer.xero.com/documentation/oauth2/overview)