@@ -2,12 +2,7 @@ import { zValidator } from '@hono/zod-validator'
22import { Hono } from 'hono'
33import { z } from 'zod'
44
5- import {
6- DefaultScopes ,
7- getAuthorizationURL ,
8- getAuthToken ,
9- refreshAuthToken ,
10- } from './cloudflare-auth'
5+ import { getAuthorizationURL , getAuthToken , refreshAuthToken } from './cloudflare-auth'
116import { McpError } from './mcp-error'
127
138import type {
@@ -140,102 +135,112 @@ export async function handleTokenExchangeCallback(
140135 }
141136}
142137
143- const app = new Hono < AuthContext > ( )
144-
145138/**
146- * OAuth Authorization Endpoint
139+ * Creates a Hono app with OAuth routes for a specific Cloudflare worker
147140 *
148- * This route initiates the Cloudflare OAuth flow when a user wants to log in.
149- * It creates a random state parameter to prevent CSRF attacks and stores the
150- * original OAuth request information in KV storage for later retrieval.
151- * Then it redirects the user to Cloudflare's authorization page with the appropriate
152- * parameters so the user can authenticate and grant permissions.
141+ * @param scopes optional subset of scopes to request when handling authorization requests
142+ * @returns a Hono app with configured OAuth routes
153143 */
154- app . get ( `/oauth/authorize` , async ( c ) => {
155- try {
156- const oauthReqInfo = await c . env . OAUTH_PROVIDER . parseAuthRequest ( c . req . raw )
157- oauthReqInfo . scope = Object . keys ( DefaultScopes )
158- if ( ! oauthReqInfo . clientId ) {
159- return c . text ( 'Invalid request' , 400 )
160- }
161-
162- const res = await getAuthorizationURL ( {
163- client_id : c . env . CLOUDFLARE_CLIENT_ID ,
164- redirect_uri : new URL ( '/oauth/callback' , c . req . url ) . href ,
165- state : oauthReqInfo ,
144+ export function createAuthHandlers ( { scopes } : { scopes : Record < string , string > } ) {
145+ {
146+ const app = new Hono < AuthContext > ( )
147+
148+ /**
149+ * OAuth Authorization Endpoint
150+ *
151+ * This route initiates the Cloudflare OAuth flow when a user wants to log in.
152+ * It creates a random state parameter to prevent CSRF attacks and stores the
153+ * original OAuth request information in KV storage for later retrieval.
154+ * Then it redirects the user to Cloudflare's authorization page with the appropriate
155+ * parameters so the user can authenticate and grant permissions.
156+ */
157+ app . get ( `/oauth/authorize` , async ( c ) => {
158+ try {
159+ const oauthReqInfo = await c . env . OAUTH_PROVIDER . parseAuthRequest ( c . req . raw )
160+ oauthReqInfo . scope = Object . keys ( scopes )
161+ if ( ! oauthReqInfo . clientId ) {
162+ return c . text ( 'Invalid request' , 400 )
163+ }
164+ const res = await getAuthorizationURL ( {
165+ client_id : c . env . CLOUDFLARE_CLIENT_ID ,
166+ redirect_uri : new URL ( '/oauth/callback' , c . req . url ) . href ,
167+ state : oauthReqInfo ,
168+ scopes,
169+ } )
170+
171+ return Response . redirect ( res . authUrl , 302 )
172+ } catch ( e ) {
173+ if ( e instanceof McpError ) {
174+ return c . text ( e . message , { status : e . code } )
175+ }
176+ console . error ( e )
177+ return c . text ( 'Internal Error' , 500 )
178+ }
166179 } )
167180
168- return Response . redirect ( res . authUrl , 302 )
169- } catch ( e ) {
170- if ( e instanceof McpError ) {
171- return c . text ( e . message , { status : e . code } )
172- }
173- console . error ( e )
174- return c . text ( 'Internal Error' , 500 )
175- }
176- } )
177-
178- /**
179- * OAuth Callback Endpoint
180- *
181- * This route handles the callback from Cloudflare after user authentication.
182- * It exchanges the temporary code for an access token, then stores some
183- * user metadata & the auth token as part of the 'props' on the token passed
184- * down to the client. It ends by redirecting the client back to _its_ callback URL
185- */
186- app . get ( `/oauth/callback` , zValidator ( 'query' , AuthQuery ) , async ( c ) => {
187- try {
188- const { state , code } = c . req . valid ( 'query' )
189- const oauthReqInfo = AuthRequestSchemaWithExtraParams . parse ( JSON . parse ( atob ( state ) ) )
190- // Get the oathReqInfo out of KV
191- if ( ! oauthReqInfo . clientId ) {
192- throw new McpError ( 'Invalid State' , 400 )
193- }
194-
195- const [ { accessToken , refreshToken , user, accounts } ] = await Promise . all ( [
196- getTokenAndUser ( c , code , oauthReqInfo . codeVerifier ) ,
197- c . env . OAUTH_PROVIDER . createClient ( {
198- clientId : oauthReqInfo . clientId ,
199- tokenEndpointAuthMethod : 'none' ,
200- } ) ,
201- ] )
202-
203- // TODO: Implement auth restriction in staging
204- // if (
205- // !user.email.endsWith("@cloudflare.com") &&
206- // !(c.env.PERMITTED_USERS ?? []).includes(user.email)
207- // ) {
208- // throw new McpError(
209- // `This user ${user .email} is not allowed to access this restricted MCP server` ,
210- // 401 ,
211- // );
212- // }
213-
214- // Return back to the MCP client a new token
215- const { redirectTo } = await c . env . OAUTH_PROVIDER . completeAuthorization ( {
216- request : oauthReqInfo ,
217- userId : user . id ,
218- metadata : {
219- label : user . email ,
220- } ,
221- scope : oauthReqInfo . scope ,
222- // This will be available on this.props inside MyMCP
223- props : {
224- user ,
225- accounts ,
226- accessToken ,
227- refreshToken ,
228- } ,
181+ /**
182+ * OAuth Callback Endpoint
183+ *
184+ * This route handles the callback from Cloudflare after user authentication.
185+ * It exchanges the temporary code for an access token, then stores some
186+ * user metadata & the auth token as part of the 'props' on the token passed
187+ * down to the client. It ends by redirecting the client back to _its_ callback URL
188+ */
189+ app . get ( `/oauth/callback` , zValidator ( 'query' , AuthQuery ) , async ( c ) => {
190+ try {
191+ const { state , code } = c . req . valid ( 'query' )
192+ const oauthReqInfo = AuthRequestSchemaWithExtraParams . parse ( JSON . parse ( atob ( state ) ) )
193+ // Get the oathReqInfo out of KV
194+ if ( ! oauthReqInfo . clientId ) {
195+ throw new McpError ( 'Invalid State' , 400 )
196+ }
197+
198+ const [ { accessToken , refreshToken , user , accounts } ] = await Promise . all ( [
199+ getTokenAndUser ( c , code , oauthReqInfo . codeVerifier ) ,
200+ c . env . OAUTH_PROVIDER . createClient ( {
201+ clientId : oauthReqInfo . clientId ,
202+ tokenEndpointAuthMethod : 'none' ,
203+ } ) ,
204+ ] )
205+
206+ // TODO: Implement auth restriction in staging
207+ // if (
208+ // ! user.email.endsWith("@cloudflare.com") &&
209+ // !(c.env.PERMITTED_USERS ?? []).includes(user.email)
210+ // ) {
211+ // throw new McpError(
212+ // `This user ${user.email} is not allowed to access this restricted MCP server` ,
213+ // 401 ,
214+ // );
215+ // }
216+
217+ // Return back to the MCP client a new token
218+ const { redirectTo } = await c . env . OAUTH_PROVIDER . completeAuthorization ( {
219+ request : oauthReqInfo ,
220+ userId : user . id ,
221+ metadata : {
222+ label : user . email ,
223+ } ,
224+ scope : oauthReqInfo . scope ,
225+ // This will be available on this.props inside MyMCP
226+ props : {
227+ user ,
228+ accounts ,
229+ accessToken ,
230+ refreshToken ,
231+ } ,
232+ } )
233+
234+ return Response . redirect ( redirectTo , 302 )
235+ } catch ( e ) {
236+ console . error ( e )
237+ if ( e instanceof McpError ) {
238+ return c . text ( e . message , { status : e . code } )
239+ }
240+ return c . text ( 'Internal Error' , 500 )
241+ }
229242 } )
230243
231- return Response . redirect ( redirectTo , 302 )
232- } catch ( e ) {
233- console . error ( e )
234- if ( e instanceof McpError ) {
235- return c . text ( e . message , { status : e . code } )
236- }
237- return c . text ( 'Internal Error' , 500 )
244+ return app
238245 }
239- } )
240-
241- export const CloudflareAuthHandler = app
246+ }
0 commit comments