Skip to main content
Glama
app.ts7.95 kB
/** * Web routes for OAuth authentication flow */ import { Hono } from "hono"; import { html } from "hono/html"; import { layout, manualRedirectPage } from "./ui"; import { createPKCECodes, generateRandomString, isTrustedRedirectUri } from "./lib"; import type { AuthRequest, OAuthHelpers } from "@cloudflare/workers-oauth-provider"; import type { StateData, GlobalpingEnv, GlobalpingOAuthTokenResponse } from "./types"; import geminiExtension from "../gemini-extension.json"; // Globalping OAuth configuration const GLOBALPING_AUTH_URL = "https://auth.globalping.io/oauth/authorize"; const GLOBALPING_TOKEN_URL = "https://auth.globalping.io/oauth/token"; const GLOBALPING_USERDATA_URL = "https://auth.globalping.io/oauth/token/introspect"; const GLOBALPING_REPOSITORY_URL = "https://github.com/jsdelivr/globalping-mcp-server"; interface Env extends GlobalpingEnv { OAUTH_PROVIDER: OAuthHelpers; } const app = new Hono<{ Bindings: Env; }>(); /** * Get user data from Globalping OAuth * @param accessToken The access token * @returns User data or null */ async function getUserData(accessToken: string): Promise<any> { const params = new URLSearchParams(); params.append("token", accessToken); const userInfoResponse = await fetch(GLOBALPING_USERDATA_URL, { method: "POST", body: params, }); if (userInfoResponse.ok) { return await userInfoResponse.json(); } return null; } // Root route - redirect to repository app.get("/", async (_c) => { return Response.redirect(GLOBALPING_REPOSITORY_URL); }); // Serve Gemini extension manifest app.get("/gemini-extension.json", (c) => { c.header("Cache-Control", "public, max-age=3600"); return c.json(geminiExtension); }); app.get("/.well-known/gemini-extension.json", (c) => { c.header("Cache-Control", "public, max-age=3600"); return c.json(geminiExtension); }); // Authorization endpoint app.get("/authorize", async (c) => { let oauthReqInfo: AuthRequest | undefined; try { oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); } catch (error: any) { return c.html( layout(await html`<h1>Invalid request</h1><p>${error.message}</p>`, "Invalid request"), ); } if (!oauthReqInfo) { return c.html( layout( await html`<h1>Invalid request</h1><p>Missing OAuth request information.</p>`, "Invalid request", ), ); } // Basic validation: just check redirect_uri is set and parsable // The callback endpoint will later decide if auto-redirect or show manual confirmation if (!oauthReqInfo.redirectUri) { return c.html( layout( await html`<h1>Invalid request</h1><p>Missing redirect URI.</p>`, "Invalid request", ), ); } try { new URL(oauthReqInfo.redirectUri); } catch (error) { return c.html( layout( await html`<h1>Invalid redirect URI</h1><p>Redirect URI is malformed.</p>`, "Invalid redirect URI", ), ); } const { codeVerifier, codeChallenge } = await createPKCECodes(); const state = generateRandomString(32); const redirectUri = `${new URL(c.req.url).origin}/auth/callback`; const clientId = c.env.GLOBALPING_CLIENT_ID; const stateData: StateData = { redirectUri: redirectUri, clientRedirectUri: oauthReqInfo.redirectUri, codeVerifier, codeChallenge, clientId, state, oauthReqInfo, createdAt: Date.now(), }; await c.env.OAUTH_KV.put(`oauth_state_${state}`, JSON.stringify(stateData), { expirationTtl: 60 * 10, // 10 minutes }); const authUrl = new URL(GLOBALPING_AUTH_URL); authUrl.searchParams.append("client_id", c.env.GLOBALPING_CLIENT_ID); authUrl.searchParams.append("response_type", "code"); authUrl.searchParams.append("redirect_uri", redirectUri); authUrl.searchParams.append("state", state); authUrl.searchParams.append("code_challenge", codeChallenge); authUrl.searchParams.append("code_challenge_method", "S256"); authUrl.searchParams.append("scope", "measurements"); return Response.redirect(authUrl.toString()); }); // OAuth callback endpoint app.get("/auth/callback", async (c) => { const code = c.req.query("code"); const state = c.req.query("state"); const error = c.req.query("error"); if (error) { return c.html( layout( await html`<h1>Authentication error</h1><p>Error: ${error}</p>`, "Authentication error", ), ); } if (!code || !state) { return c.html( layout( await html`<h1>Invalid request</h1><p>Code and state are missing</p>`, "Invalid request", ), ); } const stateKV = await c.env.OAUTH_KV.get(`oauth_state_${state}`); if (!stateKV) { return c.html( layout( await html`<h1>State is outdated</h1><p>State is outdated or missing.</p>`, "State is outdated", ), ); } let stateData: StateData | null = null; try { stateData = JSON.parse(stateKV as string) as StateData; } catch { return c.html( layout( await html`<h1>State error</h1><p>Stored state is malformed.</p>`, "State error", ), ); } await c.env.OAUTH_KV.delete(`oauth_state_${state}`); if (!stateData) { return c.html( layout( await html`<h1>State is outdated</h1><p>State is outdated or missing.</p>`, "State is outdated", ), ); } const oauthReqInfo = stateData.oauthReqInfo; if (stateData.state !== state) { return c.html( layout( await html`<h1>Invalid state</h1><p>State does not match the original request.</p>`, "Invalid state", ), ); } // Form token request using validated state data const tokenRequest = new URLSearchParams(); tokenRequest.append("grant_type", "authorization_code"); tokenRequest.append("client_id", stateData.clientId); tokenRequest.append("code", code); tokenRequest.append("redirect_uri", stateData.redirectUri); tokenRequest.append("code_verifier", stateData.codeVerifier); try { const tokenResponse = await fetch(GLOBALPING_TOKEN_URL, { method: "POST", body: tokenRequest, headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, }); if (!tokenResponse.ok) { const errorData = await tokenResponse.text(); return c.html( layout( await html`<h1>Token error</h1><p>Failed to get access token. ${JSON.stringify(errorData)}</p>`, "Token error", ), ); } const tokenData = (await tokenResponse.json()) as GlobalpingOAuthTokenResponse; tokenData.created_at = Math.floor(Date.now() / 1000); // Get user data const userData = await getUserData(tokenData.access_token); if (!userData) { return c.html( layout( await html`<h1>Authentication error</h1><p>Failed to get user data.</p>`, "Authentication error", ), ); } // Complete OAuth authorization process const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ request: oauthReqInfo, userId: userData.username, metadata: { label: userData.username, }, scope: oauthReqInfo.scope, props: { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token, clientId: oauthReqInfo.clientId, state, userName: userData.username, isAuthenticated: true, isOAuth: true, }, }); // Per OAuth 2.0 Security Best Practices (RFC 6819 section 7.12.2), // only automatically redirect to trusted URIs if (isTrustedRedirectUri(redirectTo)) { // Auto-redirect for trusted URIs (localhost, deep links) return Response.redirect(redirectTo, 302); } // Show manual confirmation page for untrusted HTTPS URIs // Extract origin for cleaner display (without query params/paths) const displayUrl = new URL(redirectTo).origin; return c.html( layout(await manualRedirectPage(redirectTo, displayUrl), "Complete Authentication"), ); } catch (error: any) { console.error("Token exchange error:", error); return c.html( layout( await html`<h1>Authentication error</h1><p>An error occurred during token exchange. ${error.message}</p>`, "Authentication error", ), ); } }); export default app;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jsdelivr/globalping-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server