import type { AuthRequest, OAuthHelpers } from '@cloudflare/workers-oauth-provider'
import { Hono, Context } from 'hono'
import { fetchUpstreamAuthToken, getUpstreamAuthorizeUrl, Props } from './oauth'
import { clientIdAlreadyApproved, parseRedirectApproval, renderApprovalDialog } from './workers-oauth-utils'
const app = new Hono<{ Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }>()
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)
}
if (await clientIdAlreadyApproved(c.req.raw, oauthReqInfo.clientId, c.env.COOKIE_ENCRYPTION_KEY)) {
// return redirectToGoogle(c, oauthReqInfo)
}
return renderApprovalDialog(c.req.raw, {
client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
server: {
provider: "google",
name: 'MCP Boilerplate',
logo: "https://avatars.githubusercontent.com/u/314135?s=200&v=4",
description: 'This is a boilerplate MCP that you can use to build your own remote MCP server, with Stripe integration for paid tools and Google/Github authentication.',
},
state: { oauthReqInfo },
})
})
app.post('/authorize', async (c) => {
const { state, headers } = await parseRedirectApproval(c.req.raw, c.env.COOKIE_ENCRYPTION_KEY)
if (!state.oauthReqInfo) {
return c.text('Invalid request', 400)
}
return redirectToGoogle(c, state.oauthReqInfo, headers)
})
async function redirectToGoogle(c: Context, oauthReqInfo: AuthRequest, headers: Record<string, string> = {}) {
return new Response(null, {
status: 302,
headers: {
...headers,
location: getUpstreamAuthorizeUrl({
upstream_url: 'https://accounts.google.com/o/oauth2/v2/auth',
scope: 'email profile',
client_id: c.env.GOOGLE_CLIENT_ID,
redirect_uri: new URL('/callback/google', c.req.raw.url).href,
state: btoa(JSON.stringify(oauthReqInfo)),
hosted_domain: c.env.HOSTED_DOMAIN,
}),
},
})
}
/**
* OAuth Callback Endpoint
*
* This route handles the callback from Google after user authentication.
* It exchanges the temporary code for an access token, then stores some
* user metadata & the auth token as part of the 'props' on the token passed
* down to the client. It ends by redirecting the client back to _its_ callback URL
*/
app.get('/callback/google', async (c) => {
// Get the oathReqInfo out of KV
const oauthReqInfo = JSON.parse(atob(c.req.query('state') as string)) as AuthRequest
if (!oauthReqInfo.clientId) {
return c.text('Invalid state', 400)
}
// Exchange the code for an access token
const code = c.req.query('code')
if (!code) {
return c.text('Missing code', 400)
}
const [accessToken, googleErrResponse] = await fetchUpstreamAuthToken({
upstream_url: 'https://accounts.google.com/o/oauth2/token',
client_id: c.env.GOOGLE_CLIENT_ID,
client_secret: c.env.GOOGLE_CLIENT_SECRET,
code,
redirect_uri: new URL('/callback/google', c.req.url).href,
grant_type: 'authorization_code',
})
if (googleErrResponse) {
return googleErrResponse
}
// Fetch the user info from Google
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
if (!userResponse.ok) {
return c.text(`Failed to fetch user info: ${await userResponse.text()}`, 500)
}
const { id, name, email } = (await userResponse.json()) as {
id: string
name: string
email: string
}
// Return back to the MCP client a new token
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
request: oauthReqInfo,
userId: id,
metadata: {
label: name,
},
scope: oauthReqInfo.scope,
props: {
name,
email,
accessToken,
userEmail: email,
} as Props,
})
return Response.redirect(redirectTo)
})
export { app as GoogleHandler }