1- // workers-oauth-utils.ts
2-
31import type { AuthRequest , ClientInfo } from "@cloudflare/workers-oauth-provider" ;
42
53const COOKIE_NAME = "__Host-MCP_APPROVED_CLIENTS" ;
64const ONE_YEAR_IN_SECONDS = 31536000 ;
75
8- // --- Helper Functions ---
9-
10-
116/**
127 * Imports a secret key string for HMAC-SHA256 signing.
138 * @param secret - The raw secret key string.
@@ -124,8 +119,6 @@ async function getApprovedClientsFromCookie(
124119 }
125120}
126121
127- // --- Exported Functions ---
128-
129122/**
130123 * Checks if a given client ID has already been approved by the user,
131124 * based on a signed cookie.
@@ -191,30 +184,25 @@ export function renderApprovalDialog(request: Request, options: ApprovalDialogOp
191184 const { client, server, state, csrfToken, setCookie } = options ;
192185 const encodedState = btoa ( JSON . stringify ( state ) ) ;
193186
194- // Sanitize any untrusted content
195187 const serverName = sanitizeHtml ( server . name ) ;
196188 const clientName = client ?. clientName ? sanitizeHtml ( client . clientName ) : "Unknown MCP Client" ;
197189 const serverDescription = server . description ? sanitizeHtml ( server . description ) : "" ;
198190
199- // Safe URLs
200191 const logoUrl = server . logo ? sanitizeHtml ( server . logo ) : "" ;
201192 const clientUri = client ?. clientUri ? sanitizeHtml ( client . clientUri ) : "" ;
202193 const policyUri = client ?. policyUri ? sanitizeHtml ( client . policyUri ) : "" ;
203194 const tosUri = client ?. tosUri ? sanitizeHtml ( client . tosUri ) : "" ;
204195
205- // Client contacts
206196 const contacts =
207197 client ?. contacts && client . contacts . length > 0
208198 ? sanitizeHtml ( client . contacts . join ( ", " ) )
209199 : "" ;
210200
211- // Get redirect URIs
212201 const redirectUris =
213202 client ?. redirectUris && client . redirectUris . length > 0
214203 ? client . redirectUris . map ( ( uri ) => sanitizeHtml ( uri ) ) . filter ( ( uri ) => uri !== "" )
215204 : [ ] ;
216205
217- // Generate HTML for the approval dialog
218206 const htmlContent = `
219207 <!DOCTYPE html>
220208 <html lang="en">
@@ -545,7 +533,6 @@ export async function parseRedirectApproval(
545533
546534 const formData = await request . formData ( ) ;
547535
548- // Validate CSRF token
549536 const tokenFromForm = formData . get ( "csrf_token" ) ;
550537 if ( ! tokenFromForm || typeof tokenFromForm !== "string" ) {
551538 throw new Error ( "Missing CSRF token in form data" ) ;
@@ -570,29 +557,24 @@ export async function parseRedirectApproval(
570557 throw new Error ( "Invalid state data" ) ;
571558 }
572559
573- // Add client to approved list
574560 const existingApprovedClients =
575561 ( await getApprovedClientsFromCookie ( request . headers . get ( "Cookie" ) , cookieSecret ) ) || [ ] ;
576562 const updatedApprovedClients = Array . from (
577563 new Set ( [ ...existingApprovedClients , state . oauthReqInfo . clientId ] ) ,
578564 ) ;
579565
580- // Sign the updated list
581566 const payload = JSON . stringify ( updatedApprovedClients ) ;
582567 const key = await importKey ( cookieSecret ) ;
583568 const signature = await signData ( key , payload ) ;
584569 const newCookieValue = `${ signature } .${ btoa ( payload ) } ` ; // signature.base64(payload)
585570
586- // Generate Set-Cookie header
587571 const headers : Record < string , string > = {
588572 "Set-Cookie" : `${ COOKIE_NAME } =${ newCookieValue } ; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=${ ONE_YEAR_IN_SECONDS } ` ,
589573 } ;
590574
591575 return { headers, state } ;
592576}
593577
594- // --- New security functions for KV state storage ---
595-
596578/**
597579 * Result from bindStateToSession containing the cookie to set
598580 */
@@ -637,15 +619,6 @@ export async function createOAuthState(
637619
638620/**
639621 * Binds an OAuth state token to the user's browser session using a secure cookie.
640- * This prevents CSRF attacks where an attacker's state token is used by a victim.
641- *
642- * SECURITY: This cookie proves that the browser completing the OAuth callback
643- * is the same browser that consented to the authorization request.
644- *
645- * We hash the state token rather than storing it directly for defense-in-depth:
646- * - Even if the state parameter leaks (URL logs, referrer headers), the cookie value cannot be derived
647- * - The cookie serves as cryptographic proof of consent, not just a copy of the state
648- * - Provides an additional layer of security beyond HttpOnly/Secure flags
649622 *
650623 * @param stateToken - The state token to bind to the session
651624 * @returns Object containing the Set-Cookie header to send to the client
0 commit comments