github-handler.tsā¢4.43 kB
// import { env } from "cloudflare:workers";
import type { AuthRequest } from "@cloudflare/workers-oauth-provider";
import { Hono } from "hono";
import { Octokit } from "octokit";
import type { Props, ExtendedEnv } from "../types";
import {
clientIdAlreadyApproved,
parseRedirectApproval,
renderApprovalDialog,
fetchUpstreamAuthToken,
getUpstreamAuthorizeUrl,
} from "./oauth-utils";
const app = new Hono<{ Bindings: ExtendedEnv }>();
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 redirectToGithub(c.req.raw, oauthReqInfo, c.env, {});
}
return renderApprovalDialog(c.req.raw, {
client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
server: {
description: "This is a demo MCP Remote Server using GitHub for authentication.",
logo: "https://avatars.githubusercontent.com/u/314135?s=200&v=4",
name: "Cloudflare GitHub MCP Server", // 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, c.env.COOKIE_ENCRYPTION_KEY);
if (!state.oauthReqInfo) {
return c.text("Invalid request", 400);
}
return redirectToGithub(c.req.raw, state.oauthReqInfo, c.env, headers);
});
async function redirectToGithub(
request: Request,
oauthReqInfo: AuthRequest,
env: Env,
headers: Record<string, string> = {},
) {
return new Response(null, {
headers: {
...headers,
location: getUpstreamAuthorizeUrl({
client_id: (env as any).GITHUB_CLIENT_ID,
redirect_uri: new URL("/callback", request.url).href,
scope: "read:user",
state: btoa(JSON.stringify(oauthReqInfo)),
upstream_url: "https://github.com/login/oauth/authorize",
}),
},
status: 302,
});
}
/**
* OAuth Callback Endpoint
*
* This route handles the callback from GitHub 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", 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 [accessToken, errResponse] = await fetchUpstreamAuthToken({
client_id: c.env.GITHUB_CLIENT_ID,
client_secret: c.env.GITHUB_CLIENT_SECRET,
code: c.req.query("code"),
redirect_uri: new URL("/callback", c.req.url).href,
upstream_url: "https://github.com/login/oauth/access_token",
});
if (errResponse) return errResponse;
// Fetch the user info from GitHub
const user = await new Octokit({ auth: accessToken }).rest.users.getAuthenticated();
const { login, name, email } = user.data;
// SECURITY: Only allow specific user to access this MCP server
const AUTHORIZED_USER = 'preangelleo';
if (login !== AUTHORIZED_USER) {
console.log(`Access denied for user: ${login}. Only ${AUTHORIZED_USER} is authorized.`);
return new Response(
`<html><body>
<h1>Access Denied</h1>
<p>Sorry, this MCP server is private and only accessible to the owner.</p>
<p>User <strong>${login}</strong> is not authorized to access this service.</p>
<p>If you believe this is an error, please contact the server administrator.</p>
</body></html>`,
{
status: 403,
headers: { 'Content-Type': 'text/html' }
}
);
}
console.log(`Access granted for authorized user: ${login}`);
// Return back to the MCP client a new token
const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
metadata: {
label: name,
},
// This will be available on this.props inside MyMCP
props: {
accessToken,
email,
login,
name,
} as Props,
request: oauthReqInfo,
scope: oauthReqInfo.scope,
userId: login,
});
return Response.redirect(redirectTo);
});
export { app as GitHubHandler };