google-handler.ts•3.85 kB
import type { AuthRequest, OAuthHelpers } from "@cloudflare/workers-oauth-provider";
import { type Context, Hono } from "hono";
import { fetchUpstreamAuthToken, getUpstreamAuthorizeUrl, type Props } from "./utils";
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, await c.env.OAUTH_KV.get('COOKIE_ENCRYPTION_KEY') ?? '')
) {
return redirectToGoogle(c, oauthReqInfo);
}
return renderApprovalDialog(c.req.raw, {
client: await c.env.OAUTH_PROVIDER.lookupClient(clientId),
server: {
description: "This MCP Server is a demo for Google OAuth.",
name: "Google OAuth Demo",
},
state: { oauthReqInfo },
});
});
app.post("/authorize", async (c) => {
const { state, headers } = await parseRedirectApproval(c.req.raw, await c.env.OAUTH_KV.get('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, {
headers: {
...headers,
location: getUpstreamAuthorizeUrl({
clientId: await c.env.OAUTH_KV.get('GOOGLE_CLIENT_ID') ?? '',
hostedDomain: await c.env.OAUTH_KV.get('HOSTED_DOMAIN') ?? '',
redirectUri: new URL("/callback", c.req.raw.url).href,
scope: "email profile",
state: btoa(JSON.stringify(oauthReqInfo)),
upstreamUrl: "https://accounts.google.com/o/oauth2/v2/auth",
}),
},
status: 302,
});
}
/**
* 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", 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({
clientId: await c.env.OAUTH_KV.get('GOOGLE_CLIENT_ID') ?? '',
clientSecret: await c.env.OAUTH_KV.get('GOOGLE_CLIENT_SECRET') ?? '',
code,
grantType: "authorization_code",
redirectUri: new URL("/callback", c.req.url).href,
upstreamUrl: "https://accounts.google.com/o/oauth2/token",
});
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({
metadata: {
label: name,
},
props: {
accessToken,
email,
name,
} as Props,
request: oauthReqInfo,
scope: oauthReqInfo.scope,
userId: id,
});
return Response.redirect(redirectTo);
});
export { app as GoogleHandler };