Skip to main content
Glama
vantage-sh

vantage-mcp-server

Official
by vantage-sh
auth.ts22.7 kB
/* Note that this code started from the examples at https://github.com/cloudflare/ai/tree/0150b265a4510123b545b4f988511bf0b63c6641/demos/remote-mcp-auth0 */ import { env } from "cloudflare:workers"; import type { AuthRequest, OAuthHelpers, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, } from "@cloudflare/workers-oauth-provider"; import axios from "axios"; import type { Context } from "hono"; import { getCookie, setCookie } from "hono/cookie"; import { html, raw } from "hono/html"; import type { JWTPayload } from "jose"; import * as oauth from "oauth4webapi"; export type UserProps = { claims: JWTPayload; tokenSet: { accessToken: string; idToken: string; refreshToken: string; }; }; type Auth0AuthRequest = { mcpAuthRequest: AuthRequest; codeVerifier: string; codeChallenge: string; nonce: string; transactionState: string; consentToken: string; }; // TODO pull from env const APP_ENV: "development" | "production" = "development"; export async function getOidcConfig({ issuer, client_id, client_secret, }: { issuer: string; client_id: string; client_secret: string; }) { const as = await oauth .discoveryRequest(new URL(issuer), { algorithm: "oidc" }) .then((response) => oauth.processDiscoveryResponse(new URL(issuer), response)); const client: oauth.Client = { client_id }; const clientAuth = oauth.ClientSecretPost(client_secret); return { as, client, clientAuth }; } export interface RequiredEnv { VANTAGE_API_HOST: string; VANTAGE_MCP_TOKEN: string; AUTH0_CLIENT_ID: string; AUTH0_CLIENT_SECRET: string; AUTH0_DOMAIN: string; AUTH0_SCOPE: string; AUTH0_AUDIENCE: string; SELF_CALLBACK_URL: string; SENTRY_DSN: string; CF_VERSION_METADATA: { id: string }; } /** * OAuth Authorization Endpoint * * This route initiates the Authorization Code Flow when a user wants to log in. * It creates a random state parameter to prevent CSRF attacks and stores the * original request information in a state-specific cookie for later retrieval. * Then it shows a consent screen before redirecting to Auth0. */ export async function authorize( c: Context<{ Bindings: RequiredEnv & { OAUTH_PROVIDER: OAuthHelpers } }> ) { const mcpClientAuthRequest = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); if (!mcpClientAuthRequest.clientId) { return c.text("Invalid request", 400); } const client = await c.env.OAUTH_PROVIDER.lookupClient(mcpClientAuthRequest.clientId); if (!client) { return c.text("Invalid client", 400); } // Generate all that is needed for the Auth0 auth request const codeVerifier = oauth.generateRandomCodeVerifier(); const transactionState = oauth.generateRandomState(); const consentToken = oauth.generateRandomState(); // For CSRF protection on consent form // We will persist everything in a cookie. const auth0AuthRequest: Auth0AuthRequest = { codeChallenge: await oauth.calculatePKCECodeChallenge(codeVerifier), codeVerifier, consentToken, mcpAuthRequest: mcpClientAuthRequest, nonce: oauth.generateRandomNonce(), transactionState, }; // Store the auth request in a transaction-specific cookie const cookieName = `auth0_req_${transactionState}`; setCookie(c, cookieName, btoa(JSON.stringify(auth0AuthRequest)), { httpOnly: true, maxAge: 60 * 60 * 1, path: "/", sameSite: APP_ENV === "production" ? "none" : "lax", secure: APP_ENV === "production", // 1 hour }); // Extract client information for the consent screen const clientName = client.clientName || client.clientId; const clientLogo = client.logoUri || ""; // No default logo const clientUri = client.clientUri || "#"; const requestedScopes = (c.env.AUTH0_SCOPE || "").split(" "); // Render the consent screen with CSRF protection return c.html( renderConsentScreen({ clientLogo, clientName, clientUri, consentToken, redirectUri: mcpClientAuthRequest.redirectUri, requestedScopes, transactionState, }) ); } /** * Consent Confirmation Endpoint * * This route handles the consent confirmation before redirecting to Auth0 */ export async function confirmConsent( c: Context<{ Bindings: RequiredEnv & { OAUTH_PROVIDER: OAuthHelpers } }> ) { // Get form data const formData = await c.req.formData(); const transactionState = formData.get("transaction_state") as string; const consentToken = formData.get("consent_token") as string; const consentAction = formData.get("consent_action") as string; // Validate the transaction state if (!transactionState) { return c.text("Invalid transaction state", 400); } // Get the transaction-specific cookie const cookieName = `auth0_req_${transactionState}`; const auth0AuthRequestCookie = getCookie(c, cookieName); if (!auth0AuthRequestCookie) { return c.text("Invalid or expired transaction", 400); } // Parse the Auth0 auth request from the cookie const auth0AuthRequest = JSON.parse(atob(auth0AuthRequestCookie)) as Auth0AuthRequest; // Validate the CSRF token if (auth0AuthRequest.consentToken !== consentToken) { return c.text("Invalid consent token", 403); } const formGivesConsent = consentAction === "approve" || consentAction === "approve-sso"; // Handle user denial if (!formGivesConsent) { // Parse the MCP client auth request to get the original redirect URI const redirectUri = new URL(auth0AuthRequest.mcpAuthRequest.redirectUri); // Add error parameters to the redirect URI redirectUri.searchParams.set("error", "access_denied"); redirectUri.searchParams.set("error_description", "User denied the request"); if (auth0AuthRequest.mcpAuthRequest.state) { redirectUri.searchParams.set("state", auth0AuthRequest.mcpAuthRequest.state); } // Clear the transaction cookie setCookie(c, cookieName, "", { maxAge: 0, path: "/", }); return c.redirect(redirectUri.toString()); } const { as } = await getOidcConfig({ client_id: c.env.AUTH0_CLIENT_ID, client_secret: c.env.AUTH0_CLIENT_SECRET, issuer: `https://${c.env.AUTH0_DOMAIN}/`, }); // Redirect to Auth0's authorization endpoint const authorizationLoginEndpoint = consentAction === "approve-sso" ? await getSSOLoginUrl(formData.get("sso_email") as string, c.env) : as.authorization_endpoint!; const authorizationUrl = new URL(authorizationLoginEndpoint); if (consentAction === "approve-sso") { authorizationUrl.host = c.env.AUTH0_DOMAIN; } authorizationUrl.searchParams.set("client_id", c.env.AUTH0_CLIENT_ID); authorizationUrl.searchParams.set("redirect_uri", c.env.SELF_CALLBACK_URL); authorizationUrl.searchParams.set("response_type", "code"); authorizationUrl.searchParams.set("audience", c.env.AUTH0_AUDIENCE); authorizationUrl.searchParams.set("scope", c.env.AUTH0_SCOPE); authorizationUrl.searchParams.set("code_challenge", auth0AuthRequest.codeChallenge); authorizationUrl.searchParams.set("code_challenge_method", "S256"); authorizationUrl.searchParams.set("nonce", auth0AuthRequest.nonce); authorizationUrl.searchParams.set("state", transactionState); return c.redirect(authorizationUrl.href); } async function getSSOLoginUrl(ssoEmail: string, env: RequiredEnv): Promise<string> { const requestOptions = { url: `${env.VANTAGE_API_HOST}/internal/email_identity_provider`, method: "GET", params: { email: ssoEmail, callback_url: env.SELF_CALLBACK_URL, scope: env.AUTH0_SCOPE, }, }; const apiResult = await axios(requestOptions); if (apiResult.status !== 200) { throw new Error(`Failed to get Auth0 connection for ${ssoEmail}: ${apiResult.statusText}`); } const location = apiResult.data?.location; if (!location) { throw new Error(`No location found in Auth0 connection response for ${ssoEmail}`); } return location; } /** * OAuth Callback Endpoint * * This route handles the callback from Auth0 after user authentication. * It exchanges the authorization code for tokens and completes the * authorization process. */ export async function callback( c: Context<{ Bindings: RequiredEnv & { OAUTH_PROVIDER: OAuthHelpers } }> ) { // Parse the state parameter to extract transaction state and Auth0 state const stateParam = c.req.query("state") as string; if (!stateParam) { return c.text("Invalid state parameter", 400); } // Parse the Auth0 auth request from the transaction-specific cookie const cookieName = `auth0_req_${stateParam}`; const auth0AuthRequestCookie = getCookie(c, cookieName); if (!auth0AuthRequestCookie) { return c.text("Invalid transaction state or session expired", 400); } const auth0AuthRequest = JSON.parse(atob(auth0AuthRequestCookie)) as Auth0AuthRequest; // Clear the transaction cookie as it's no longer needed setCookie(c, cookieName, "", { maxAge: 0, path: "/", }); const { as, client, clientAuth } = await getOidcConfig({ client_id: c.env.AUTH0_CLIENT_ID, client_secret: c.env.AUTH0_CLIENT_SECRET, issuer: `https://${c.env.AUTH0_DOMAIN}/`, }); // Perform the Code Exchange const params = oauth.validateAuthResponse( as, client, new URL(c.req.url), auth0AuthRequest.transactionState ); const response = await oauth.authorizationCodeGrantRequest( as, client, clientAuth, params, new URL("/callback", c.req.url).href, auth0AuthRequest.codeVerifier ); // Process the response const result = await oauth.processAuthorizationCodeResponse(as, client, response, { expectedNonce: auth0AuthRequest.nonce, requireIdToken: true, }); // Get the claims from the id_token const claims = oauth.getValidatedIdTokenClaims(result); if (!claims) { return c.text("Received invalid id_token from Auth0", 400); } // Complete the authorization const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({ metadata: { label: claims.name || claims.email || claims.sub, }, props: { claims: claims, tokenSet: { accessToken: result.access_token, accessTokenTTL: result.expires_in, idToken: result.id_token, refreshToken: result.refresh_token, }, } as UserProps, request: auth0AuthRequest.mcpAuthRequest, scope: auth0AuthRequest.mcpAuthRequest.scope, userId: claims.sub!, }); return Response.redirect(redirectTo); } /** * Token Exchange Callback * * This function handles the token exchange callback for the CloudflareOAuth Provider and allows us to then interact with the Upstream IdP (your Auth0 tenant) */ export async function tokenExchangeCallback( options: TokenExchangeCallbackOptions ): Promise<TokenExchangeCallbackResult | undefined> { // During the Authorization Code Exchange, we want to make sure that the Access Token issued // by the MCP Server has the same TTL as the one issued by Auth0. if (options.grantType === "authorization_code") { return { accessTokenTTL: options.props.tokenSet.accessTokenTTL, newProps: { ...options.props, }, }; } if (options.grantType === "refresh_token") { const auth0RefreshToken = options.props.tokenSet.refreshToken; if (!auth0RefreshToken) { throw new Error("No Auth0 refresh token found"); } const { as, client, clientAuth } = await getOidcConfig({ client_id: (env as RequiredEnv).AUTH0_CLIENT_ID, client_secret: (env as RequiredEnv).AUTH0_CLIENT_SECRET, issuer: `https://${(env as RequiredEnv).AUTH0_DOMAIN}/`, }); // Perform the refresh token exchange with Auth0. const response = await oauth.refreshTokenGrantRequest( as, client, clientAuth, auth0RefreshToken ); const refreshTokenResponse = await oauth.processRefreshTokenResponse(as, client, response); // Get the claims from the id_token const claims = oauth.getValidatedIdTokenClaims(refreshTokenResponse); if (!claims) { throw new Error("Received invalid id_token from Auth0"); } // Store the new token set and claims. return { accessTokenTTL: refreshTokenResponse.expires_in, newProps: { ...options.props, claims: claims, tokenSet: { accessToken: refreshTokenResponse.access_token, accessTokenTTL: refreshTokenResponse.expires_in, idToken: refreshTokenResponse.id_token, refreshToken: refreshTokenResponse.refresh_token || auth0RefreshToken, }, }, }; } } /** * Renders the consent screen HTML */ export function renderConsentScreen({ clientName, // clientLogo, // TODO: Implement logo display // clientUri, // TODO: Implement client URI display redirectUri, requestedScopes, transactionState, consentToken, }: { clientName: string; clientLogo: string; clientUri: string; redirectUri: string; requestedScopes: string[]; transactionState: string; consentToken: string; }) { return html` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Authorization Request</title> <style> :root { --primary-color: #872EE1; --text-color: #333; --background-color: #f7f7f7; --card-background: #ffffff; --border-color: #e0e0e0; --danger-color: #ef233c; --success-color: #2a9d8f; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } body { font-family: var(--font-family); background-color: var(--background-color); color: var(--text-color); margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { width: 100%; max-width: 480px; padding: 20px; } .card { background-color: var(--card-background); border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); padding: 32px; overflow: hidden; } .header { text-align: center; margin-bottom: 24px; } h1 { font-size: 20px; } .app-link { color: var(--primary-color); text-decoration: none; font-size: 14px; } .app-link:hover { text-decoration: underline; } .description { margin: 24px 0; font-size: 16px; line-height: 1.5; } .description:first-of-type { margin: 0 0 24px 0; } .scopes { background-color: var(--background-color); border-radius: 8px; padding: 16px; margin: 24px 0; } .scope-title { font-weight: 600; margin-bottom: 8px; font-size: 15px; } .scope-list { font-size: 16px; margin: 0; padding-left: 20px; } .actions { display: flex; gap: 12px; margin-top: 24px; } .btn { flex: 1; padding: 12px 20px; font-size: 16px; font-weight: 500; border-radius: 8px; cursor: pointer; border: none; transition: all 0.2s ease; } input[type="text"].btn { cursor: auto; border-radius: 0px; } .btn-cancel { background-color: transparent; border: 1px solid var(--border-color); color: var(--text-color); } .btn-cancel:hover { background-color: rgba(0, 0, 0, 0.05); } .btn-approve { color: var(--background-color); border-radius: 6px; border: 0px solid var(--primary-color); background: var(--primary-color); } .btn-approve:hover { background: var(--primary-color); } .security-note { margin-top: 24px; font-size: 12px; color: #777; text-align: center; } @media (max-width: 520px) { .container { padding: 10px; } .card { padding: 24px; border-radius: 8px; } } .app-logo { width: 40px; height: 40px; object-fit: contain; border-radius: 8px; } .app-logo-mcp { width: 24px; height: 24px; object-fit: contain; border-radius: 8px; padding: 8px; } .app-logo.shadow, .app-logo-mcp.shadow { border-radius: 80px; background: var(--background-color, #FFF); box-shadow: 0px 5px 12px 0px rgba(0, 0, 0, 0.10); } .img-container { display: inline-block; margin: 0 8px; } .mb-8 { margin-bottom: 8px; } </style> </head> <body> <div class="container"> <div class="card"> <div class="header"> <h1>Vantage Hosted MCP Server<br>Authorization Request</h1> </div> <div class="header"> <span class="img-container"> <img src="/vantage-logo.svg" alt="Vantage logo" class="shadow app-logo" /> </span> <img src="/line.svg" alt="line" class="app-logo" /> <span class="img-container"> <img src="/mcp-logo.png" alt="Model Context Protocol logo" class="shadow app-logo-mcp" /> </span> </div> <p class="description"> <strong>${clientName}</strong> is requesting permission to access the <strong>Vantage API</strong> using your account. Please review the permissions before proceeding. </p> <p class="description mb-8"> By clicking "Allow Access", you authorize <strong>${clientName}</strong> to access the following resources: </p> <ul class="scope-list"> ${raw(requestedScopes.map((scope) => `<li>${scope}</li>`).join("\n"))} </ul> <p class="description"> If you did not initiate the request coming from <strong>${clientName}</strong> (<i>${redirectUri}</i>) or you do not trust this application, you should deny access. </p> <form method="POST" action="/authorize/consent"> <input type="hidden" name="transaction_state" value="${transactionState}" /> <input type="hidden" name="consent_token" value="${consentToken}" /> <div class="actions"> <button type="submit" name="consent_action" value="deny" class="btn btn-cancel">Deny Access</button> <button type="submit" name="consent_action" value="approve" class="btn btn-approve">Allow Access</button> </div> </form> <p class="description"> Users that access Vantage through their SSO provider, please provide your email address below: </p> <form method="POST" action="/authorize/consent"> <input type="hidden" name="transaction_state" value="${transactionState}" /> <input type="hidden" name="consent_token" value="${consentToken}" /> <div class="actions"> <input type="text" name="sso_email" placeholder="SSO Login Email" class="btn btn-cancel" required> <button type="submit" name="consent_action" value="approve-sso" class="btn btn-approve">Allow SSO</button> </div> </form> <p class="security-note"> You're signing in to a third-party application. Your account information is never shared without your permission. </p> </div> </div> </body> </html> `; }

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/vantage-sh/vantage-mcp-server'

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