router.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import express from 'express';
  2. import { clientRegistrationHandler } from './handlers/register.js';
  3. import { tokenHandler } from './handlers/token.js';
  4. import { authorizationHandler } from './handlers/authorize.js';
  5. import { revocationHandler } from './handlers/revoke.js';
  6. import { metadataHandler } from './handlers/metadata.js';
  7. // Check for dev mode flag that allows HTTP issuer URLs (for development/testing only)
  8. const allowInsecureIssuerUrl = process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === 'true' || process.env.MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL === '1';
  9. if (allowInsecureIssuerUrl) {
  10. // eslint-disable-next-line no-console
  11. console.warn('MCP_DANGEROUSLY_ALLOW_INSECURE_ISSUER_URL is enabled - HTTP issuer URLs are allowed. Do not use in production.');
  12. }
  13. const checkIssuerUrl = (issuer) => {
  14. // Technically RFC 8414 does not permit a localhost HTTPS exemption, but this will be necessary for ease of testing
  15. if (issuer.protocol !== 'https:' && issuer.hostname !== 'localhost' && issuer.hostname !== '127.0.0.1' && !allowInsecureIssuerUrl) {
  16. throw new Error('Issuer URL must be HTTPS');
  17. }
  18. if (issuer.hash) {
  19. throw new Error(`Issuer URL must not have a fragment: ${issuer}`);
  20. }
  21. if (issuer.search) {
  22. throw new Error(`Issuer URL must not have a query string: ${issuer}`);
  23. }
  24. };
  25. export const createOAuthMetadata = (options) => {
  26. const issuer = options.issuerUrl;
  27. const baseUrl = options.baseUrl;
  28. checkIssuerUrl(issuer);
  29. const authorization_endpoint = '/authorize';
  30. const token_endpoint = '/token';
  31. const registration_endpoint = options.provider.clientsStore.registerClient ? '/register' : undefined;
  32. const revocation_endpoint = options.provider.revokeToken ? '/revoke' : undefined;
  33. const metadata = {
  34. issuer: issuer.href,
  35. service_documentation: options.serviceDocumentationUrl?.href,
  36. authorization_endpoint: new URL(authorization_endpoint, baseUrl || issuer).href,
  37. response_types_supported: ['code'],
  38. code_challenge_methods_supported: ['S256'],
  39. token_endpoint: new URL(token_endpoint, baseUrl || issuer).href,
  40. token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
  41. grant_types_supported: ['authorization_code', 'refresh_token'],
  42. scopes_supported: options.scopesSupported,
  43. revocation_endpoint: revocation_endpoint ? new URL(revocation_endpoint, baseUrl || issuer).href : undefined,
  44. revocation_endpoint_auth_methods_supported: revocation_endpoint ? ['client_secret_post'] : undefined,
  45. registration_endpoint: registration_endpoint ? new URL(registration_endpoint, baseUrl || issuer).href : undefined
  46. };
  47. return metadata;
  48. };
  49. /**
  50. * Installs standard MCP authorization server endpoints, including dynamic client registration and token revocation (if supported).
  51. * Also advertises standard authorization server metadata, for easier discovery of supported configurations by clients.
  52. * Note: if your MCP server is only a resource server and not an authorization server, use mcpAuthMetadataRouter instead.
  53. *
  54. * By default, rate limiting is applied to all endpoints to prevent abuse.
  55. *
  56. * This router MUST be installed at the application root, like so:
  57. *
  58. * const app = express();
  59. * app.use(mcpAuthRouter(...));
  60. */
  61. export function mcpAuthRouter(options) {
  62. const oauthMetadata = createOAuthMetadata(options);
  63. const router = express.Router();
  64. router.use(new URL(oauthMetadata.authorization_endpoint).pathname, authorizationHandler({ provider: options.provider, ...options.authorizationOptions }));
  65. router.use(new URL(oauthMetadata.token_endpoint).pathname, tokenHandler({ provider: options.provider, ...options.tokenOptions }));
  66. router.use(mcpAuthMetadataRouter({
  67. oauthMetadata,
  68. // Prefer explicit RS; otherwise fall back to AS baseUrl, then to issuer (back-compat)
  69. resourceServerUrl: options.resourceServerUrl ?? options.baseUrl ?? new URL(oauthMetadata.issuer),
  70. serviceDocumentationUrl: options.serviceDocumentationUrl,
  71. scopesSupported: options.scopesSupported,
  72. resourceName: options.resourceName
  73. }));
  74. if (oauthMetadata.registration_endpoint) {
  75. router.use(new URL(oauthMetadata.registration_endpoint).pathname, clientRegistrationHandler({
  76. clientsStore: options.provider.clientsStore,
  77. ...options.clientRegistrationOptions
  78. }));
  79. }
  80. if (oauthMetadata.revocation_endpoint) {
  81. router.use(new URL(oauthMetadata.revocation_endpoint).pathname, revocationHandler({ provider: options.provider, ...options.revocationOptions }));
  82. }
  83. return router;
  84. }
  85. export function mcpAuthMetadataRouter(options) {
  86. checkIssuerUrl(new URL(options.oauthMetadata.issuer));
  87. const router = express.Router();
  88. const protectedResourceMetadata = {
  89. resource: options.resourceServerUrl.href,
  90. authorization_servers: [options.oauthMetadata.issuer],
  91. scopes_supported: options.scopesSupported,
  92. resource_name: options.resourceName,
  93. resource_documentation: options.serviceDocumentationUrl?.href
  94. };
  95. // Serve PRM at the path-specific URL per RFC 9728
  96. const rsPath = new URL(options.resourceServerUrl.href).pathname;
  97. router.use(`/.well-known/oauth-protected-resource${rsPath === '/' ? '' : rsPath}`, metadataHandler(protectedResourceMetadata));
  98. // Always add this for OAuth Authorization Server metadata per RFC 8414
  99. router.use('/.well-known/oauth-authorization-server', metadataHandler(options.oauthMetadata));
  100. return router;
  101. }
  102. /**
  103. * Helper function to construct the OAuth 2.0 Protected Resource Metadata URL
  104. * from a given server URL. This replaces the path with the standard metadata endpoint.
  105. *
  106. * @param serverUrl - The base URL of the protected resource server
  107. * @returns The URL for the OAuth protected resource metadata endpoint
  108. *
  109. * @example
  110. * getOAuthProtectedResourceMetadataUrl(new URL('https://api.example.com/mcp'))
  111. * // Returns: 'https://api.example.com/.well-known/oauth-protected-resource/mcp'
  112. */
  113. export function getOAuthProtectedResourceMetadataUrl(serverUrl) {
  114. const u = new URL(serverUrl.href);
  115. const rsPath = u.pathname && u.pathname !== '/' ? u.pathname : '';
  116. return new URL(`/.well-known/oauth-protected-resource${rsPath}`, u).href;
  117. }
  118. //# sourceMappingURL=router.js.map