app.ts•3.5 kB
import { Hono } from 'hono';
import {
  layout,
  homeContent,
  parseApproveFormBody,
  renderAuthorizationApprovedContent,
  renderLoggedOutAuthorizeScreen,
  renderAuthorizationRejectedContent,
} from './utils';
import type { OAuthHelpers } from '@cloudflare/workers-oauth-provider';
import { McpOptions } from 'dodopayments-mcp/server';
import { ServerConfig } from '.';
export type Bindings = Env & {
  OAUTH_PROVIDER: OAuthHelpers;
};
export function makeOAuthConsent(config: ServerConfig, defaultOptions?: Partial<McpOptions>) {
  const app = new Hono<{
    Bindings: Bindings;
  }>();
  // Render a reasonable home page just to show the app is up
  app.get('/', async (c) => {
    const content = await homeContent(c.req.raw);
    return c.html(layout(content, 'Home', config));
  });
  // The /authorize page has a form that will POST to /approve
  app.get('/authorize', async (c) => {
    const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw);
    const content = await renderLoggedOutAuthorizeScreen(config, oauthReqInfo, defaultOptions);
    return c.html(layout(content, 'Authorization', config));
  });
  // This endpoint is responsible for validating any login information and
  // then completing the authorization request with the OAUTH_PROVIDER
  app.post('/approve', async (c) => {
    const { action, oauthReqInfo, clientProps, clientConfig } = await parseApproveFormBody(
      await c.req.parseBody(),
      config,
    );
    if (action !== 'login_approve') {
      return c.html(
        layout(
          await renderAuthorizationRejectedContent(oauthReqInfo?.redirectUri || ''),
          'Authorization Status',
          config,
        ),
      );
    }
    if (!oauthReqInfo || !clientProps || !clientConfig) {
      return c.html('INVALID LOGIN', 401);
    }
    // We don't have a real user ID, just tokens, so we generate a random one
    // Make this some stable ID if you want to look up the user's grants later.
    const generatedUserId = crypto.randomUUID();
    const { redirectTo } = await c.env.OAUTH_PROVIDER.completeAuthorization({
      request: oauthReqInfo,
      userId: generatedUserId,
      metadata: {},
      scope: oauthReqInfo.scope,
      props: {
        clientProps,
        clientConfig,
      },
    });
    return c.html(
      layout(await renderAuthorizationApprovedContent(redirectTo), 'Authorization Status', config),
    );
  });
  // Render the authorize screen for demoing the OAuth flow (it won't actually log in)
  app.get('/demo', async (c) => {
    const content = await renderLoggedOutAuthorizeScreen(config, {} as any, defaultOptions);
    return c.html(layout(content, 'Authorization', config));
  });
  // Add a resource server .well-known to point clients to the correct auth server
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': '*',
    'Access-Control-Max-Age': '86400',
  };
  app.options('/.well-known/oauth-protected-resource', async (c) => {
    Object.entries(corsHeaders).forEach(([key, value]) => c.header(key, value));
    return c.body(null, 204);
  });
  app.get('/.well-known/oauth-protected-resource', async (c) => {
    Object.entries(corsHeaders).forEach(([key, value]) => c.header(key, value));
    const baseURL = new URL('/', c.req.url).toString();
    return c.json({
      resource: baseURL,
      authorization_servers: [baseURL],
    });
  });
  return app;
}