mcp oauth cloudflare

安装量: 293
排名: #3086

安装

npx skills add https://github.com/jezweb/claude-skills --skill 'MCP OAuth Cloudflare'
MCP OAuth Cloudflare
Production-ready OAuth authentication for MCP servers on Cloudflare Workers.
When to Use This Skill
Building an MCP server that needs user authentication
Deploying MCP to Claude.ai (requires Dynamic Client Registration)
Replacing static auth tokens with OAuth for better security
Adding Google Sign-In to your MCP server
Need user context (email, name, picture) in MCP tool handlers
When NOT to Use
Internal/private MCP servers where tokens are acceptable
MCP servers without user-specific data
Local-only MCP development (use tokens for simplicity)
Architecture Overview
Dual OAuth Role Pattern
When using a third-party OAuth provider (like Google), the MCP Server acts as
both an OAuth client (to upstream service) and as an OAuth server (to MCP clients)
. The Worker:
Stores encrypted access token in Workers KV
Issues its own token to the client
workers-oauth-provider
handles spec compliance
Critical
The MCP server generates and issues its own token rather than passing through the third-party token. This is essential for security and spec compliance. ┌─────────────────────────────────────────────────────────────────────┐ │ Cloudflare Worker │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────┐ ┌──────────────────────────────────┐ │ │ │ OAuthProvider │ │ McpAgent (Durable Object) │ │ │ │ ───────────────── │ │ ──────────────────────────── │ │ │ │ /register (DCR) │ │ MCP Tools with user props: │ │ │ │ /authorize │─────▶│ - this.props.email │ │ │ │ /token │ │ - this.props.id │ │ │ │ /mcp │ │ - this.props.accessToken │ │ │ └─────────────────────┘ └──────────────────────────────────┘ │ │ │ │ │ │ OAuth Flow │ │ ▼ │ │ ┌─────────────────────┐ ┌──────────────────────────────────┐ │ │ │ Google Handler │ │ KV Namespace (OAUTH_KV) │ │ │ │ ───────────────── │ │ ──────────────────────────── │ │ │ │ /authorize (GET) │─────▶│ oauth:state:{token} → AuthReq │ │ │ │ /authorize (POST) │ │ TTL: 10 minutes │ │ │ │ /callback │ └──────────────────────────────────┘ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ Quick Start 1. Install Dependencies npm install @cloudflare/workers-oauth-provider agents @modelcontextprotocol/sdk hono zod 2. Create OAuth Directory Structure src/ ├── index.ts # Main entry with OAuthProvider └── oauth/ ├── google-handler.ts # OAuth routes (/authorize, /callback) ├── utils.ts # Google token exchange & user info └── workers-oauth-utils.ts # CSRF, state validation, approval UI 3. Configure wrangler.jsonc { "name": "my-mcp-server", "main": "src/index.ts", "compatibility_flags": ["nodejs_compat"], // KV for OAuth state storage "kv_namespaces": [ { "binding": "OAUTH_KV", "id": "YOUR_KV_NAMESPACE_ID" } ], // Durable Objects for MCP sessions "durable_objects": { "bindings": [ { "class_name": "MyMcpServer", "name": "MCP_OBJECT" } ] }, "migrations": [ { "new_sqlite_classes": ["MyMcpServer"], "tag": "v1" } ] } 4. Set Secrets

Google OAuth credentials (from console.cloud.google.com)

echo "YOUR_GOOGLE_CLIENT_ID" | npx wrangler secret put GOOGLE_CLIENT_ID echo "YOUR_GOOGLE_CLIENT_SECRET" | npx wrangler secret put GOOGLE_CLIENT_SECRET

Cookie encryption key (32+ chars)

python3 -c "import secrets; print(secrets.token_urlsafe(32))" | npx wrangler secret put COOKIE_ENCRYPTION_KEY

Optional: Custom Google OAuth scopes (default: 'openid email profile')

See "Common Google Scopes" section below for scope recipes

echo "openid email profile https://www.googleapis.com/auth/drive" | npx wrangler secret put GOOGLE_SCOPES

Deploy to activate secrets

npx wrangler deploy
5. Type Definitions (Optional but Recommended)
Copy
templates/env.d.ts
to
src/env.d.ts
for TypeScript type support:
interface
Env
{
GOOGLE_CLIENT_ID
:
string
;
GOOGLE_CLIENT_SECRET
:
string
;
COOKIE_ENCRYPTION_KEY
:
string
;
GOOGLE_SCOPES
?
:
string
;
// Optional: Override default scopes
OAUTH_KV
:
KVNamespace
;
MCP_OBJECT
:
DurableObjectNamespace
;
}
Implementation Guide
Main Entry Point (index.ts)
import
OAuthProvider
from
'@cloudflare/workers-oauth-provider'
;
import
{
McpServer
}
from
'@modelcontextprotocol/sdk/server/mcp.js'
;
import
{
McpAgent
}
from
'agents/mcp'
;
import
{
z
}
from
'zod'
;
import
{
GoogleHandler
}
from
'./oauth/google-handler'
;
// Props from OAuth - user info stored in token
type
Props
=
{
id
:
string
;
email
:
string
;
name
:
string
;
picture
?
:
string
;
accessToken
:
string
;
refreshToken
?
:
string
;
// Available on first auth with access_type=offline
}
;
export
class
MyMcpServer
extends
McpAgent
<
Env
,
Record
<
string
,
never
>
,
Props
>
{
server
=
new
McpServer
(
{
name
:
'my-mcp-server'
,
version
:
'1.0.0'
,
}
)
;
async
init
(
)
{
// Register tools - user info available via this.props
this
.
server
.
tool
(
'my_tool'
,
'Tool description'
,
{
param
:
z
.
string
(
)
}
,
async
(
args
)
=>
{
// Access authenticated user
const
userEmail
=
this
.
props
?.
email
;
console
.
log
(
`
Tool called by:
${
userEmail
}
`
)
;
return
{
content
:
[
{
type
:
'text'
,
text
:
'Result'
}
]
}
;
}
)
;
}
}
// Wrap with OAuth provider
export
default
new
OAuthProvider
(
{
apiHandlers
:
{
'/sse'
:
MyMcpServer
.
serveSSE
(
'/sse'
)
,
'/mcp'
:
MyMcpServer
.
serve
(
'/mcp'
)
,
}
,
authorizeEndpoint
:
'/authorize'
,
clientRegistrationEndpoint
:
'/register'
,
defaultHandler
:
GoogleHandler
as
any
,
tokenEndpoint
:
'/token'
,
}
)
;
Google Handler (oauth/google-handler.ts)
import
{
env
}
from
'cloudflare:workers'
;
import
type
{
AuthRequest
,
OAuthHelpers
}
from
'@cloudflare/workers-oauth-provider'
;
import
{
Hono
}
from
'hono'
;
import
{
fetchUpstreamAuthToken
,
fetchGoogleUserInfo
,
getUpstreamAuthorizeUrl
,
type
Props
}
from
'./utils'
;
import
{
addApprovedClient
,
bindStateToSession
,
createOAuthState
,
generateCSRFProtection
,
isClientApproved
,
OAuthError
,
renderApprovalDialog
,
validateCSRFToken
,
validateOAuthState
,
}
from
'./workers-oauth-utils'
;
const
app
=
new
Hono
<
{
Bindings
:
Env
&
{
OAUTH_PROVIDER
:
OAuthHelpers
}
}
>
(
)
;
// GET /authorize - Show approval dialog or redirect to Google
app
.
get
(
'/authorize'
,
async
(
c
)
=>
{
const
oauthReqInfo
=
await
c
.
env
.
OAUTH_PROVIDER
.
parseAuthRequest
(
c
.
req
.
raw
)
;
const
{
clientId
}
=
oauthReqInfo
;
if
(
!
clientId
)
return
c
.
text
(
'Invalid request'
,
400
)
;
// Skip approval if client already approved
if
(
await
isClientApproved
(
c
.
req
.
raw
,
clientId
,
env
.
COOKIE_ENCRYPTION_KEY
)
)
{
const
{
stateToken
}
=
await
createOAuthState
(
oauthReqInfo
,
c
.
env
.
OAUTH_KV
)
;
const
{
setCookie
}
=
await
bindStateToSession
(
stateToken
)
;
return
redirectToGoogle
(
c
.
req
.
raw
,
stateToken
,
{
'Set-Cookie'
:
setCookie
}
)
;
}
// Show approval dialog with CSRF protection
const
{
token
:
csrfToken
,
setCookie
}
=
generateCSRFProtection
(
)
;
return
renderApprovalDialog
(
c
.
req
.
raw
,
{
client
:
await
c
.
env
.
OAUTH_PROVIDER
.
lookupClient
(
clientId
)
,
csrfToken
,
server
:
{
name
:
'My MCP Server'
,
description
:
'Description of your server'
,
logo
:
'https://example.com/logo.png'
,
}
,
setCookie
,
state
:
{
oauthReqInfo
}
,
}
)
;
}
)
;
// POST /authorize - Process approval form
app
.
post
(
'/authorize'
,
async
(
c
)
=>
{
try
{
const
formData
=
await
c
.
req
.
raw
.
formData
(
)
;
validateCSRFToken
(
formData
,
c
.
req
.
raw
)
;
const
encodedState
=
formData
.
get
(
'state'
)
as
string
;
const
state
=
JSON
.
parse
(
atob
(
encodedState
)
)
;
// Add to approved clients
const
approvedCookie
=
await
addApprovedClient
(
c
.
req
.
raw
,
state
.
oauthReqInfo
.
clientId
,
c
.
env
.
COOKIE_ENCRYPTION_KEY
)
;
// Create state and redirect
const
{
stateToken
}
=
await
createOAuthState
(
state
.
oauthReqInfo
,
c
.
env
.
OAUTH_KV
)
;
const
{
setCookie
}
=
await
bindStateToSession
(
stateToken
)
;
const
headers
=
new
Headers
(
)
;
headers
.
append
(
'Set-Cookie'
,
approvedCookie
)
;
headers
.
append
(
'Set-Cookie'
,
setCookie
)
;
return
redirectToGoogle
(
c
.
req
.
raw
,
stateToken
,
Object
.
fromEntries
(
headers
)
)
;
}
catch
(
error
:
any
)
{
if
(
error
instanceof
OAuthError
)
return
error
.
toResponse
(
)
;
return
c
.
text
(
`
Error:
${
error
.
message
}
`
,
500
)
;
}
}
)
;
// GET /callback - Handle Google OAuth callback
app
.
get
(
'/callback'
,
async
(
c
)
=>
{
const
{
oauthReqInfo
,
clearCookie
}
=
await
validateOAuthState
(
c
.
req
.
raw
,
c
.
env
.
OAUTH_KV
)
;
// Exchange code for token
const
[
accessToken
,
err
]
=
await
fetchUpstreamAuthToken
(
{
client_id
:
c
.
env
.
GOOGLE_CLIENT_ID
,
client_secret
:
c
.
env
.
GOOGLE_CLIENT_SECRET
,
code
:
c
.
req
.
query
(
'code'
)
,
redirect_uri
:
new
URL
(
'/callback'
,
c
.
req
.
url
)
.
href
,
upstream_url
:
'https://oauth2.googleapis.com/token'
,
}
)
;
if
(
err
)
return
err
;
// Get user info
const
user
=
await
fetchGoogleUserInfo
(
accessToken
)
;
if
(
!
user
)
return
c
.
text
(
'Failed to fetch user info'
,
500
)
;
// Complete authorization
const
{
redirectTo
}
=
await
c
.
env
.
OAUTH_PROVIDER
.
completeAuthorization
(
{
props
:
{
accessToken
,
email
:
user
.
email
,
id
:
user
.
id
,
name
:
user
.
name
,
picture
:
user
.
picture
,
}
as
Props
,
request
:
oauthReqInfo
,
scope
:
oauthReqInfo
.
scope
,
userId
:
user
.
id
,
}
)
;
return
new
Response
(
null
,
{
status
:
302
,
headers
:
{
Location
:
redirectTo
,
'Set-Cookie'
:
clearCookie
}
,
}
)
;
}
)
;
async
function
redirectToGoogle
(
request
:
Request
,
stateToken
:
string
,
headers
:
Record
<
string
,
string
>
=
{
}
)
{
// Scopes configurable via GOOGLE_SCOPES env var (see "Common Google Scopes" section)
const
scopes
=
env
.
GOOGLE_SCOPES
||
'openid email profile'
;
return
new
Response
(
null
,
{
status
:
302
,
headers
:
{
...
headers
,
location
:
getUpstreamAuthorizeUrl
(
{
client_id
:
env
.
GOOGLE_CLIENT_ID
,
redirect_uri
:
new
URL
(
'/callback'
,
request
.
url
)
.
href
,
scope
:
scopes
,
state
:
stateToken
,
upstream_url
:
'https://accounts.google.com/o/oauth2/v2/auth'
,
}
)
,
}
,
}
)
;
}
export
{
app
as
GoogleHandler
}
;
OAuth Flow Diagram
User clicks "Connect" in Claude.ai
┌─────────────────────────────────┐
│ 1. /register (DCR) │ ◄── Claude.ai registers as client
│ Returns client credentials │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 2. GET /authorize │
│ - Check approved clients │
│ - Show approval dialog │
│ - Generate CSRF token │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 3. POST /authorize │
│ - Validate CSRF │
│ - Create state in KV │
│ - Redirect to Google │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 4. Google OAuth │
│ - User signs in │
│ - Consents to scopes │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 5. GET /callback │
│ - Validate state │
│ - Exchange code for token │
│ - Fetch user info │
│ - Complete authorization │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ 6. User props available │
│ this.props.email │
│ this.props.id │
│ this.props.accessToken │
└─────────────────────────────────┘
Security Features
CSRF Protection
// Generate CSRF token with HttpOnly cookie
export
function
generateCSRFProtection
(
)
:
CSRFProtectionResult
{
const
token
=
crypto
.
randomUUID
(
)
;
const
setCookie
=
`
__Host-CSRF_TOKEN=
${
token
}
; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600
`
;
return
{
token
,
setCookie
}
;
}
State Validation (Prevents Replay Attacks)
// Create one-time-use state in KV
export
async
function
createOAuthState
(
oauthReqInfo
:
AuthRequest
,
kv
:
KVNamespace
)
{
const
stateToken
=
crypto
.
randomUUID
(
)
;
await
kv
.
put
(
`
oauth:state:
${
stateToken
}
`
,
JSON
.
stringify
(
oauthReqInfo
)
,
{
expirationTtl
:
600
,
// 10 minutes
}
)
;
return
{
stateToken
}
;
}
Session Binding (Prevents Token Theft)
// Bind state to browser session via SHA-256 hash
export
async
function
bindStateToSession
(
stateToken
:
string
)
{
const
hashBuffer
=
await
crypto
.
subtle
.
digest
(
'SHA-256'
,
new
TextEncoder
(
)
.
encode
(
stateToken
)
)
;
const
hashHex
=
Array
.
from
(
new
Uint8Array
(
hashBuffer
)
)
.
map
(
b
=>
b
.
toString
(
16
)
.
padStart
(
2
,
'0'
)
)
.
join
(
''
)
;
const
setCookie
=
`
__Host-CONSENTED_STATE=
${
hashHex
}
; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600
`
;
return
{
setCookie
}
;
}
Client Approval Caching (Reduces Consent Fatigue)
// HMAC-signed cookie tracks approved clients (30-day TTL)
export
async
function
addApprovedClient
(
request
:
Request
,
clientId
:
string
,
cookieSecret
:
string
)
{
const
existing
=
await
getApprovedClientsFromCookie
(
request
,
cookieSecret
)
||
[
]
;
const
updated
=
[
...
new
Set
(
[
...
existing
,
clientId
]
)
]
;
const
payload
=
JSON
.
stringify
(
updated
)
;
const
signature
=
await
signData
(
payload
,
cookieSecret
)
;
return
`
__Host-APPROVED_CLIENTS=
${
signature
}
.
${
btoa
(
payload
)
}
; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=2592000
`
;
}
PKCE Methods (Current Limitation)
Note
The library currently accepts both
plain
and
S256
PKCE methods. There is no configuration option to enforce S256-only, which is the OAuth 2.1 recommended method.
Security Consideration
For maximum security, you may want S256-only. This is tracked in
GitHub Issue #113
as a feature request.
Workaround
Until this is configurable, the library will accept both methods. Modern OAuth clients (including Claude.ai) use S256 by default. Google Cloud Console Setup Go to console.cloud.google.com Create new project or select existing Navigate to APIs & Services → Credentials Click Create Credentials → OAuth client ID Application type: Web application Add authorized redirect URI: https://your-worker.workers.dev/callback Copy Client ID and Client Secret Common Google Scopes Configure scopes via the GOOGLE_SCOPES environment variable or modify the redirectToGoogle function. Use Case Scopes Basic user info (default) openid email profile Google Drive (full access) openid email profile https://www.googleapis.com/auth/drive Google Drive (file-level only) openid email profile https://www.googleapis.com/auth/drive.file Google Docs openid email profile https://www.googleapis.com/auth/documents Google Docs + Drive openid email profile https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents Gmail (read/send) openid email profile https://www.googleapis.com/auth/gmail.modify Gmail (read only) openid email profile https://www.googleapis.com/auth/gmail.readonly Google Calendar openid email profile https://www.googleapis.com/auth/calendar Google Sheets openid email profile https://www.googleapis.com/auth/spreadsheets Google Slides openid email profile https://www.googleapis.com/auth/presentations YouTube Data openid email profile https://www.googleapis.com/auth/youtube Setting Scopes:

Option 1: Via environment variable (recommended for flexibility)

echo "openid email profile https://www.googleapis.com/auth/drive" | npx wrangler secret put GOOGLE_SCOPES

Option 2: In wrangler.jsonc (for non-sensitive scopes)

{
"vars"
:
{
"GOOGLE_SCOPES"
:
"openid email profile https://www.googleapis.com/auth/drive"
}
}
Important Notes:
Always include
openid email profile
- required for user identification
Additional scopes must be enabled in Google Cloud Console (APIs & Services → Library)
Some scopes require OAuth consent screen verification for production use
drive.file
only accesses files the app created or user explicitly opened with it
Refresh Token Lifecycle (v0.2.0+)
For long-lived sessions (Google APIs, Gmail, Drive), you need refresh tokens.
Design Decision: Two Valid Refresh Tokens
Note
:
@cloudflare/workers-oauth-provider
implements a non-standard refresh token rotation strategy. At any time, a grant may have
two valid refresh tokens
. When the client uses one, the other is invalidated and a new one is generated.
Why It Differs from OAuth 2.1
OAuth 2.1 requires single-use refresh tokens for public clients. However, the library author argues that single-use tokens are fundamentally flawed because they assume every refresh request completes with no errors. In the real world, network errors or software faults could mean the client fails to store the new refresh token.
Security Trade-off
Allowing the previous refresh token disables replay attack detection. For confidential clients (most MCP servers), this is compliant with OAuth 2.1. For public clients, consider stricter rotation if needed.
Source
:
GitHub Issue #43
, documented in
README
Requesting Refresh Tokens
Add
access_type=offline
to the authorization URL:
// In google-handler.ts, redirectToGoogle function
googleAuthUrl
.
searchParams
.
set
(
'access_type'
,
'offline'
)
;
googleAuthUrl
.
searchParams
.
set
(
'prompt'
,
'consent'
)
;
// Forces new refresh token
When to use
access_type=offline
:
MCP server needs to call Google APIs after initial auth
Long-running sessions (background tasks, scheduled jobs)
User data synchronization
When to use
access_type=online
(default):
Simple user identification only
No API calls beyond initial auth
Short sessions (admin login, one-time actions)
Storing Refresh Tokens
Store encrypted in your Props type:
export
type
Props
=
{
id
:
string
;
email
:
string
;
name
:
string
;
picture
?
:
string
;
accessToken
:
string
;
refreshToken
?
:
string
;
// Store when received
tokenExpiresAt
?
:
number
;
// Track expiration
}
;
Refreshing Expired Tokens
export
async
function
refreshAccessToken
(
client_id
:
string
,
client_secret
:
string
,
refresh_token
:
string
)
:
Promise
<
{
accessToken
:
string
;
expiresAt
:
number
}
|
null
>
{
const
resp
=
await
fetch
(
'https://oauth2.googleapis.com/token'
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/x-www-form-urlencoded'
}
,
body
:
new
URLSearchParams
(
{
client_id
,
client_secret
,
refresh_token
,
grant_type
:
'refresh_token'
,
}
)
.
toString
(
)
,
}
)
;
if
(
!
resp
.
ok
)
return
null
;
// Token revoked, requires re-auth
const
body
=
await
resp
.
json
(
)
;
return
{
accessToken
:
body
.
access_token
,
expiresAt
:
Date
.
now
(
)
+
(
body
.
expires_in
*
1000
)
,
}
;
}
When Refresh Tokens Become Invalid
User revokes access at
https://myaccount.google.com/permissions
User changes password (if using certain scopes)
Token unused for 6+ months
OAuth app credentials regenerated
Handle gracefully
Catch refresh failures and redirect to re-authorize.
Bearer Token + OAuth Coexistence
Modern MCP servers support
both
OAuth (Claude.ai) and Bearer tokens (CLI tools, ElevenLabs):
// In your main fetch handler
export
default
{
async
fetch
(
request
:
Request
,
env
:
Env
,
ctx
:
ExecutionContext
)
{
const
authHeader
=
request
.
headers
.
get
(
'Authorization'
)
;
const
url
=
new
URL
(
request
.
url
)
;
// Check for Bearer token auth on MCP endpoints
if
(
env
.
AUTH_TOKEN
&&
authHeader
?.
startsWith
(
'Bearer '
)
&&
(
url
.
pathname
===
'/sse'
||
url
.
pathname
===
'/mcp'
)
)
{
const
token
=
authHeader
.
slice
(
7
)
;
if
(
token
===
env
.
AUTH_TOKEN
)
{
// Programmatic access (CLI, ElevenLabs)
const
headerAuthCtx
=
{
...
ctx
,
props
:
{
source
:
'bearer'
}
}
;
return
mcpHandler
.
fetch
(
request
,
env
,
headerAuthCtx
)
;
}
// NOT env.AUTH_TOKEN - fall through to OAuth provider
// (it may be an OAuth token from Claude.ai)
}
// OAuth flow for web clients
return
oauthProvider
.
fetch
(
request
,
env
,
ctx
)
;
}
}
;
Critical Pattern
Non-matching Bearer tokens must fall through to OAuth provider, not return 401. OAuth tokens from Claude.ai are also sent as Bearer tokens. Adding AUTH_TOKEN secret: python3 -c "import secrets; print(secrets.token_urlsafe(32))" | npx wrangler secret put AUTH_TOKEN npx wrangler deploy

Required to activate

Common Issues
"Invalid state" Error
Cause
State expired (>10 min) or KV lookup failed
Fix
Restart the OAuth flow - states are one-time-use
"CSRF token mismatch"
Cause
Form submitted without matching cookie
Fix
Ensure cookies are enabled and not blocked by browser extensions
Claude.ai Shows "Connection Failed"
Cause
Missing DCR endpoint or invalid response
Fix
Ensure
clientRegistrationEndpoint: '/register'
is set in OAuthProvider config
User Props Undefined
Cause
Accessing
this.props
before OAuth completes
Fix
Check
if (this.props)
before accessing user data
OAuth vs Auth Tokens Comparison
Aspect
Auth Tokens
OAuth
Token sharing
Manual (risky)
Automatic
User consent
None
Explicit approval
Expiration
Manual
Automatic refresh
Revocation
None built-in
User can disconnect
Scope
All-or-nothing
Fine-grained
Claude.ai compatible
No (DCR required)
Yes
Required Secrets
Secret
Purpose
Generate
GOOGLE_CLIENT_ID
OAuth app ID
Google Cloud Console
GOOGLE_CLIENT_SECRET
OAuth app secret
Google Cloud Console
COOKIE_ENCRYPTION_KEY
Sign approval cookies
secrets.token_urlsafe(32)
GOOGLE_SCOPES
(optional)
Override default OAuth scopes
See "Common Google Scopes" section
Token Efficiency
Without Skill
With Skill
Savings
~20k tokens, 3-5 attempts
~6k tokens, first try
~70%
Known Issues Prevention
This skill prevents
9
documented errors.
Issue #1: RFC 8707 Audience Validation Fails with Path Components (v0.1.0+)
Error
:
invalid_token: Token audience does not match resource server
Source
:
GitHub Issue #108
Affects
v0.1.0+ when using RFC 8707 resource indicators with paths (e.g., ChatGPT custom connectors)
Why It Happens
The
resourceServer
is computed using only the origin (
https://example.com
) but RFC 8707 recommends using full URLs with paths (
https://example.com/api
). The strict equality check in
audienceMatches
fails when:
Token audience:
https://example.com/api
(from
resource
parameter)
Resource server:
https://example.com
(computed from request URL origin only)
Prevention
:
If using RFC 8707 resource indicators with paths, vendor the library and modify
handleApiRequest
:
// Workaround: Include pathname in resourceServer computation
const
resourceServer
=
`
${
requestUrl
.
protocol
}
//
${
requestUrl
.
host
}
${
requestUrl
.
pathname
}
`
;
Or avoid using paths in resource indicators until this is fixed upstream.
Issue #2: Claude.ai Client Cannot Connect (v0.2.2)
Error
Claude.ai MCP client fails to connect during OAuth flow
Source
:
GitHub Issue #133
Affects
v0.2.2, Claude.ai MCP clients
Why It Happens
There is a '/' character in the
audienceMatches
function that prevents Claude.ai from connecting. Likely related to Issue #1 (RFC 8707 path handling).
Prevention
Monitor
Issue #133
for updates. This may require a library update or vendoring the library with a fix.
Issue #3: Props Not Updated After Re-authorization (Upstream OAuth Expiry)
Error
Infinite re-auth loop when upstream OAuth provider doesn't provide refresh tokens
Source
:
GitHub Issue #34
Affects
MCP servers using upstream OAuth providers without refresh tokens
Why It Happens
Throwing
invalid_grant
in
tokenExchangeCallback
triggers re-authorization, but
completeAuthorization()
doesn't update props. Stale props cause repeated auth failures until the OAuth client restarts.
Prevention
:
If your upstream OAuth provider doesn't issue refresh tokens:
Implement a fallback strategy (store token expiry, re-auth before expiration)
Monitor
Issue #34
for official fix
Consider client restart as temporary workaround
Problematic Pattern:
tokenExchangeCallback
:
async
(
options
)
=>
{
if
(
options
.
grantType
===
"refresh_token"
)
{
const
response
=
await
fetchNewToken
(
options
.
props
.
accessToken
)
;
if
(
!
response
.
ok
)
{
// Triggers re-auth but props remain stale
throw
new
Error
(
JSON
.
stringify
(
{
error
:
"invalid_grant"
,
error_description
:
"access token expired"
}
)
)
;
}
}
}
Issue #4: Redirect URI Mismatch in Production (Development vs Production Behavior)
Error
:
Invalid redirect URI. The redirect URI provided does not match any registered URI for this client
Source
:
GitHub Issue #29
(Community-sourced)
Affects
Production deployments; works fine in local
wrangler dev
Why It Happens
Dynamic Client Registration (DCR) behavior differs between local and production environments. Redirect URIs auto-register during DCR, but something fails in production. Root cause unclear but affecting multiple users with MCP clients (Cursor, Windsurf, PyCharm).
Prevention
:
Explicitly register redirect URIs when possible instead of relying on DCR auto-registration
Test OAuth flow in production environment before deploying to Claude.ai
Monitor
Issue #29
for resolution
Issue #5: CSRF Vulnerabilities
Error
Session hijacking, OAuth callback interception
Prevention
HttpOnly cookies with SameSite attribute
const
setCookie
=
`
__Host-CSRF_TOKEN=
${
token
}
; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=600
`
;
Issue #6: State Replay Attacks
Error
OAuth state reused across multiple authorization attempts
Prevention
One-time-use KV state with 10-minute TTL
await
kv
.
put
(
`
oauth:state:
${
stateToken
}
`
,
JSON
.
stringify
(
oauthReqInfo
)
,
{
expirationTtl
:
600
,
}
)
;
Issue #7: Token Theft via Session Fixation
Error
OAuth state stolen and used from different browser session
Prevention
Session binding via SHA-256 hash
const
hashBuffer
=
await
crypto
.
subtle
.
digest
(
'SHA-256'
,
new
TextEncoder
(
)
.
encode
(
stateToken
)
)
;
const
hashHex
=
Array
.
from
(
new
Uint8Array
(
hashBuffer
)
)
.
map
(
b
=>
b
.
toString
(
16
)
.
padStart
(
2
,
'0'
)
)
.
join
(
''
)
;
Issue #8: Missing Dynamic Client Registration (DCR)
Error
Claude.ai shows "Connection Failed" when trying to connect
Prevention
OAuthProvider handles DCR automatically via
clientRegistrationEndpoint: '/register'
Issue #9: Cookie Tampering
Error
Approved clients list modified to bypass consent
Prevention
HMAC signatures on approval cookies
const
signature
=
await
signData
(
payload
,
cookieSecret
)
;
const
cookie
=
`
__Host-APPROVED_CLIENTS=
${
signature
}
.
${
btoa
(
payload
)
}
`
;
Version History & Breaking Changes
v0.2.2 (2025-12-20) - Current
New Features
:
Client ID Metadata Document (CIMD) support - allows HTTPS URLs as
client_id
values
Matches new MCP authorization spec:
https://modelcontextprotocol.io/specification/draft/basic/authorization
Migration
No breaking changes. CIMD support is additive.
v0.1.0 (2025-11-07)
New Features
:
Audience validation for OAuth tokens per RFC 7519
Breaking Changes
:
Tokens now require correct
aud
claim
May break existing deployments without audience validation
See Issue #108 for RFC 8707 path handling bug
Migration
:
Ensure all tokens include correct
aud
claim
Test audience validation thoroughly
If using resource indicators with paths, apply workaround from Issue #108
v0.0.x (Pre-November 2025)
Initial releases without audience validation.
Errors Prevented
RFC 8707 audience path bugs
- Workaround for path component validation
Claude.ai connection failures
- Known issue tracking
Re-auth loops
- Props update handling
Production redirect URI mismatches
- Testing and explicit registration
CSRF vulnerabilities
- HttpOnly cookies with SameSite
State replay attacks
- One-time-use KV state
Token theft
- Session binding via SHA-256
Missing DCR
- OAuthProvider handles automatically
Cookie tampering
- HMAC signatures
References
Cloudflare Workers OAuth Provider
GitHub - cloudflare/workers-oauth-provider
MCP Specification
MCP Authorization Spec
Google OAuth Documentation
Cloudflare Agents SDK
Last verified
2026-01-21 |
Skill version
2.0.0 |
Changes
Added 4 new known issues from post-training-cutoff research (RFC 8707 audience bugs, Claude.ai connection failures, re-auth loops, production redirect URI mismatches), version history section, refresh token rotation design decision, dual OAuth role pattern emphasis, and PKCE limitation note. Updated from 6 to 9 documented error preventions.
返回排行榜