Skip to content

Conversation

@geelen
Copy link
Contributor

@geelen geelen commented Apr 10, 2025

This should resolve #51. For the moment all the logic is in workers-oauth-utils.ts, but will be moved to an NPM package soon.

We can't stop GitHub from treating the MCP server as the "client" and therefore skipping the permissions dialog when the same user auths twice. So, instead, we show an interstitial approval screen:

image

If the user accepts, the client ID is stored in a cookie so future authentication requests will skip this screen too. But, importantly, if the same user tries a different MCP client (e.g. Claude Desktop using mcp-remote), they'll get prompted. This way the MCP Server doesn't auth you without at least showing you which client is asking for permission, which was the issue in #51.

Bit of tidying to do to make this more generic, but for the moment you use it with following API:

import { clientIdAlreadyApproved, parseRedirectApproval, renderApprovalDialog } from './workers-oauth-utils'

// ...

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, env.COOKIE_ENCRYPTION_KEY)) {
		return redirectToGithub(c.req.raw, oauthReqInfo)
	}

	return renderApprovalDialog(c.req.raw, {
		client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
		server: {
			name: "Cloudflare GitHub MCP Server",
			logo: "https://avatars.githubusercontent.com/u/314135?s=200&v=4",
			description: 'This is a demo MCP Remote Server using GitHub for authentication.', // optional
		},
		state: { oauthReqInfo }, // arbitrary data that flows through the form submission below
	})
})

app.post('/authorize', async (c) => {
	// Validates form submission, extracts state, and generates Set-Cookie headers to skip approval dialog next time
	const { state, headers } = await parseRedirectApproval(c.req.raw, env.COOKIE_ENCRYPTION_KEY)
	if (!state.oauthReqInfo) {
		return c.text('Invalid request', 400)
	}

	return redirectToGithub(c.req.raw, state.oauthReqInfo, headers)
})

async function redirectToGithub(request: Request, oauthReqInfo: AuthRequest, headers: Record<string, string> = {}) {
	return new Response(null, {
		status: 302,
		headers: {
			...headers,
			location: getUpstreamAuthorizeUrl({
				upstream_url: 'https://github.com/login/oauth/authorize',
				scope: 'read:user',
				client_id: env.GITHUB_CLIENT_ID,
				redirect_uri: new URL('/callback', request.url).href,
				state: btoa(JSON.stringify(oauthReqInfo)),
			}),
		},
	})
}
  • clientIdAlreadyApproved checks the cookie and skips the render if the user's already accepted
  • renderApprovalDialog renders a HTML form with the state encoded so context is preserved when you submit
  • parseRedirectApproval pulls the state out of the form submission and returns it along with the set-cookie headers to attach to the redirect.

You can test this out using my deployed version: https://mcp-github-oauth.glen.workers.dev/sse

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remote-mcp-github-oauth sample leaks tokens

1 participant