mcp4openapi 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -63
- package/dist/scripts/validate-profile.js +3 -3
- package/dist/scripts/validate-profile.js.map +1 -1
- package/dist/src/argument-normalizer.d.ts +5 -0
- package/dist/src/argument-normalizer.d.ts.map +1 -0
- package/dist/src/argument-normalizer.js +61 -0
- package/dist/src/argument-normalizer.js.map +1 -0
- package/dist/src/auth/oauth-provider.d.ts +131 -0
- package/dist/src/auth/oauth-provider.d.ts.map +1 -0
- package/dist/src/auth/oauth-provider.js +839 -0
- package/dist/src/auth/oauth-provider.js.map +1 -0
- package/dist/src/cli-config.d.ts +9 -0
- package/dist/src/cli-config.d.ts.map +1 -0
- package/dist/src/cli-config.js +111 -0
- package/dist/src/cli-config.js.map +1 -0
- package/dist/src/core/cli-config.d.ts +9 -0
- package/dist/src/core/cli-config.d.ts.map +1 -0
- package/dist/src/core/cli-config.js +125 -0
- package/dist/src/core/cli-config.js.map +1 -0
- package/dist/src/core/constants.d.ts +86 -0
- package/dist/src/core/constants.d.ts.map +1 -0
- package/dist/src/core/constants.js +86 -0
- package/dist/src/core/constants.js.map +1 -0
- package/dist/src/core/errors.d.ts +59 -0
- package/dist/src/core/errors.d.ts.map +1 -0
- package/dist/src/core/errors.js +119 -0
- package/dist/src/core/errors.js.map +1 -0
- package/dist/src/core/filtering.d.ts +19 -0
- package/dist/src/core/filtering.d.ts.map +1 -0
- package/dist/src/core/filtering.js +292 -0
- package/dist/src/core/filtering.js.map +1 -0
- package/dist/src/core/index.d.ts +26 -0
- package/dist/src/core/index.d.ts.map +1 -0
- package/dist/src/core/index.js +276 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/lib.d.ts +8 -0
- package/dist/src/core/lib.d.ts.map +1 -0
- package/dist/src/core/lib.js +7 -0
- package/dist/src/core/lib.js.map +1 -0
- package/dist/src/core/logger.d.ts +59 -0
- package/dist/src/core/logger.d.ts.map +1 -0
- package/dist/src/core/logger.js +197 -0
- package/dist/src/core/logger.js.map +1 -0
- package/dist/src/core/metrics.d.ts +97 -0
- package/dist/src/core/metrics.d.ts.map +1 -0
- package/dist/src/core/metrics.js +273 -0
- package/dist/src/core/metrics.js.map +1 -0
- package/dist/src/core/naming-warnings.d.ts +23 -0
- package/dist/src/core/naming-warnings.d.ts.map +1 -0
- package/dist/src/core/naming-warnings.js +83 -0
- package/dist/src/core/naming-warnings.js.map +1 -0
- package/dist/src/core/naming.d.ts +58 -0
- package/dist/src/core/naming.d.ts.map +1 -0
- package/dist/src/core/naming.js +510 -0
- package/dist/src/core/naming.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +15 -6
- package/dist/src/errors.js.map +1 -1
- package/dist/src/filtering.d.ts +19 -0
- package/dist/src/filtering.d.ts.map +1 -0
- package/dist/src/filtering.js +292 -0
- package/dist/src/filtering.js.map +1 -0
- package/dist/src/generated-schemas.d.ts +290 -79
- package/dist/src/generated-schemas.d.ts.map +1 -1
- package/dist/src/generated-schemas.js +17 -2
- package/dist/src/generated-schemas.js.map +1 -1
- package/dist/src/http-transport-config.d.ts +6 -0
- package/dist/src/http-transport-config.d.ts.map +1 -0
- package/dist/src/http-transport-config.js +47 -0
- package/dist/src/http-transport-config.js.map +1 -0
- package/dist/src/http-transport.d.ts +63 -13
- package/dist/src/http-transport.d.ts.map +1 -1
- package/dist/src/http-transport.js +1045 -482
- package/dist/src/http-transport.js.map +1 -1
- package/dist/src/index.d.ts +1 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -170
- package/dist/src/index.js.map +1 -1
- package/dist/src/interceptors.d.ts +1 -0
- package/dist/src/interceptors.d.ts.map +1 -1
- package/dist/src/interceptors.js +73 -63
- package/dist/src/interceptors.js.map +1 -1
- package/dist/src/lib.d.ts +1 -7
- package/dist/src/lib.d.ts.map +1 -1
- package/dist/src/lib.js +1 -6
- package/dist/src/lib.js.map +1 -1
- package/dist/src/logger.d.ts +5 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +9 -1
- package/dist/src/logger.js.map +1 -1
- package/dist/src/mcp/mcp-server-manager.d.ts +20 -0
- package/dist/src/mcp/mcp-server-manager.d.ts.map +1 -0
- package/dist/src/mcp/mcp-server-manager.js +38 -0
- package/dist/src/mcp/mcp-server-manager.js.map +1 -0
- package/dist/src/mcp/mcp-server.d.ts +205 -0
- package/dist/src/mcp/mcp-server.d.ts.map +1 -0
- package/dist/src/mcp/mcp-server.js +1473 -0
- package/dist/src/mcp/mcp-server.js.map +1 -0
- package/dist/src/mcp-server-manager.d.ts +20 -0
- package/dist/src/mcp-server-manager.d.ts.map +1 -0
- package/dist/src/mcp-server-manager.js +38 -0
- package/dist/src/mcp-server-manager.js.map +1 -0
- package/dist/src/mcp-server.d.ts +28 -0
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +406 -109
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/metrics.d.ts +11 -0
- package/dist/src/metrics.d.ts.map +1 -1
- package/dist/src/metrics.js +61 -0
- package/dist/src/metrics.js.map +1 -1
- package/dist/src/oauth-provider.d.ts +5 -0
- package/dist/src/oauth-provider.d.ts.map +1 -1
- package/dist/src/oauth-provider.js +29 -1
- package/dist/src/oauth-provider.js.map +1 -1
- package/dist/src/openapi/openapi-parser.d.ts +70 -0
- package/dist/src/openapi/openapi-parser.d.ts.map +1 -0
- package/dist/src/openapi/openapi-parser.js +458 -0
- package/dist/src/openapi/openapi-parser.js.map +1 -0
- package/dist/src/profile/profile-loader.d.ts +78 -0
- package/dist/src/profile/profile-loader.d.ts.map +1 -0
- package/dist/src/profile/profile-loader.js +490 -0
- package/dist/src/profile/profile-loader.js.map +1 -0
- package/dist/src/profile/profile-registry.d.ts +19 -0
- package/dist/src/profile/profile-registry.d.ts.map +1 -0
- package/dist/src/profile/profile-registry.js +43 -0
- package/dist/src/profile/profile-registry.js.map +1 -0
- package/dist/src/profile/profile-resolver.d.ts +41 -0
- package/dist/src/profile/profile-resolver.d.ts.map +1 -0
- package/dist/src/profile/profile-resolver.js +324 -0
- package/dist/src/profile/profile-resolver.js.map +1 -0
- package/dist/src/profile/startup-profile.d.ts +17 -0
- package/dist/src/profile/startup-profile.d.ts.map +1 -0
- package/dist/src/profile/startup-profile.js +30 -0
- package/dist/src/profile/startup-profile.js.map +1 -0
- package/dist/src/profile/startup-validation.d.ts +11 -0
- package/dist/src/profile/startup-validation.d.ts.map +1 -0
- package/dist/src/profile/startup-validation.js +21 -0
- package/dist/src/profile/startup-validation.js.map +1 -0
- package/dist/src/profile-loader.d.ts +1 -0
- package/dist/src/profile-loader.d.ts.map +1 -1
- package/dist/src/profile-loader.js +14 -3
- package/dist/src/profile-loader.js.map +1 -1
- package/dist/src/profile-registry.d.ts +18 -0
- package/dist/src/profile-registry.d.ts.map +1 -0
- package/dist/src/profile-registry.js +26 -0
- package/dist/src/profile-registry.js.map +1 -0
- package/dist/src/profile-resolver.d.ts +19 -0
- package/dist/src/profile-resolver.d.ts.map +1 -0
- package/dist/src/profile-resolver.js +167 -0
- package/dist/src/profile-resolver.js.map +1 -0
- package/dist/src/proxy-executor.d.ts.map +1 -1
- package/dist/src/proxy-executor.js +7 -0
- package/dist/src/proxy-executor.js.map +1 -1
- package/dist/src/startup-profile.d.ts +17 -0
- package/dist/src/startup-profile.d.ts.map +1 -0
- package/dist/src/startup-profile.js +30 -0
- package/dist/src/startup-profile.js.map +1 -0
- package/dist/src/startup-validation.d.ts +11 -0
- package/dist/src/startup-validation.d.ts.map +1 -0
- package/dist/src/startup-validation.js +21 -0
- package/dist/src/startup-validation.js.map +1 -0
- package/dist/src/testing/dynamic-mock-server.d.ts +24 -0
- package/dist/src/testing/dynamic-mock-server.d.ts.map +1 -0
- package/dist/src/testing/dynamic-mock-server.js +138 -0
- package/dist/src/testing/dynamic-mock-server.js.map +1 -0
- package/dist/src/testing/request-assertions.d.ts +5 -0
- package/dist/src/testing/request-assertions.d.ts.map +1 -0
- package/dist/src/testing/request-assertions.js +165 -0
- package/dist/src/testing/request-assertions.js.map +1 -0
- package/dist/src/testing/template-utils.d.ts +10 -0
- package/dist/src/testing/template-utils.d.ts.map +1 -0
- package/dist/src/testing/template-utils.js +72 -0
- package/dist/src/testing/template-utils.js.map +1 -0
- package/dist/src/testing/test-http-utils.d.ts +1 -1
- package/dist/src/testing/test-http-utils.d.ts.map +1 -1
- package/dist/src/testing/test-http-utils.js +1 -1
- package/dist/src/testing/test-http-utils.js.map +1 -1
- package/dist/src/testing/test-loader.d.ts +6 -0
- package/dist/src/testing/test-loader.d.ts.map +1 -0
- package/dist/src/testing/test-loader.js +212 -0
- package/dist/src/testing/test-loader.js.map +1 -0
- package/dist/src/testing/test-schema.d.ts +1270 -0
- package/dist/src/testing/test-schema.d.ts.map +1 -0
- package/dist/src/testing/test-schema.js +76 -0
- package/dist/src/testing/test-schema.js.map +1 -0
- package/dist/src/tool-filter/compat.d.ts +49 -0
- package/dist/src/tool-filter/compat.d.ts.map +1 -0
- package/dist/src/tool-filter/compat.js +72 -0
- package/dist/src/tool-filter/compat.js.map +1 -0
- package/dist/src/tool-filter/config/env-config-parser.d.ts +38 -0
- package/dist/src/tool-filter/config/env-config-parser.d.ts.map +1 -0
- package/dist/src/tool-filter/config/env-config-parser.js +103 -0
- package/dist/src/tool-filter/config/env-config-parser.js.map +1 -0
- package/dist/src/tool-filter/config/header-config-parser.d.ts +37 -0
- package/dist/src/tool-filter/config/header-config-parser.d.ts.map +1 -0
- package/dist/src/tool-filter/config/header-config-parser.js +118 -0
- package/dist/src/tool-filter/config/header-config-parser.js.map +1 -0
- package/dist/src/tool-filter/errors.d.ts +18 -0
- package/dist/src/tool-filter/errors.d.ts.map +1 -0
- package/dist/src/tool-filter/errors.js +21 -0
- package/dist/src/tool-filter/errors.js.map +1 -0
- package/dist/src/tool-filter/filter/filter-engine.d.ts +45 -0
- package/dist/src/tool-filter/filter/filter-engine.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/filter-engine.js +94 -0
- package/dist/src/tool-filter/filter/filter-engine.js.map +1 -0
- package/dist/src/tool-filter/filter/filter-rules.d.ts +44 -0
- package/dist/src/tool-filter/filter/filter-rules.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/filter-rules.js +72 -0
- package/dist/src/tool-filter/filter/filter-rules.js.map +1 -0
- package/dist/src/tool-filter/filter/global-tool-filter.d.ts +40 -0
- package/dist/src/tool-filter/filter/global-tool-filter.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/global-tool-filter.js +92 -0
- package/dist/src/tool-filter/filter/global-tool-filter.js.map +1 -0
- package/dist/src/tool-filter/filter/session-tool-filter.d.ts +29 -0
- package/dist/src/tool-filter/filter/session-tool-filter.d.ts.map +1 -0
- package/dist/src/tool-filter/filter/session-tool-filter.js +69 -0
- package/dist/src/tool-filter/filter/session-tool-filter.js.map +1 -0
- package/dist/src/tool-filter/index.d.ts +25 -0
- package/dist/src/tool-filter/index.d.ts.map +1 -0
- package/dist/src/tool-filter/index.js +30 -0
- package/dist/src/tool-filter/index.js.map +1 -0
- package/dist/src/tool-filter/integration/tool-filter-service.d.ts +44 -0
- package/dist/src/tool-filter/integration/tool-filter-service.d.ts.map +1 -0
- package/dist/src/tool-filter/integration/tool-filter-service.js +68 -0
- package/dist/src/tool-filter/integration/tool-filter-service.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-classifier.d.ts +20 -0
- package/dist/src/tool-filter/operation/operation-classifier.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-classifier.js +26 -0
- package/dist/src/tool-filter/operation/operation-classifier.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-detector.d.ts +30 -0
- package/dist/src/tool-filter/operation/operation-detector.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-detector.js +96 -0
- package/dist/src/tool-filter/operation/operation-detector.js.map +1 -0
- package/dist/src/tool-filter/operation/operation-resolver.d.ts +22 -0
- package/dist/src/tool-filter/operation/operation-resolver.d.ts.map +1 -0
- package/dist/src/tool-filter/operation/operation-resolver.js +32 -0
- package/dist/src/tool-filter/operation/operation-resolver.js.map +1 -0
- package/dist/src/tool-filter/regex/regex-compiler.d.ts +22 -0
- package/dist/src/tool-filter/regex/regex-compiler.d.ts.map +1 -0
- package/dist/src/tool-filter/regex/regex-compiler.js +56 -0
- package/dist/src/tool-filter/regex/regex-compiler.js.map +1 -0
- package/dist/src/tool-filter/regex/regex-validator.d.ts +24 -0
- package/dist/src/tool-filter/regex/regex-validator.d.ts.map +1 -0
- package/dist/src/tool-filter/regex/regex-validator.js +58 -0
- package/dist/src/tool-filter/regex/regex-validator.js.map +1 -0
- package/dist/src/tool-filter/types.d.ts +92 -0
- package/dist/src/tool-filter/types.d.ts.map +1 -0
- package/dist/src/tool-filter/types.js +5 -0
- package/dist/src/tool-filter/types.js.map +1 -0
- package/dist/src/tool-filter/utils.d.ts +11 -0
- package/dist/src/tool-filter/utils.d.ts.map +1 -0
- package/dist/src/tool-filter/utils.js +13 -0
- package/dist/src/tool-filter/utils.js.map +1 -0
- package/dist/src/tool-filter.d.ts +65 -0
- package/dist/src/tool-filter.d.ts.map +1 -0
- package/dist/src/tool-filter.js +471 -0
- package/dist/src/tool-filter.js.map +1 -0
- package/dist/src/tool-generator.d.ts +1 -0
- package/dist/src/tool-generator.d.ts.map +1 -1
- package/dist/src/tool-generator.js +15 -6
- package/dist/src/tool-generator.js.map +1 -1
- package/dist/src/tooling/composite-executor.d.ts +77 -0
- package/dist/src/tooling/composite-executor.d.ts.map +1 -0
- package/dist/src/tooling/composite-executor.js +198 -0
- package/dist/src/tooling/composite-executor.js.map +1 -0
- package/dist/src/tooling/dag-executor.d.ts +49 -0
- package/dist/src/tooling/dag-executor.d.ts.map +1 -0
- package/dist/src/tooling/dag-executor.js +138 -0
- package/dist/src/tooling/dag-executor.js.map +1 -0
- package/dist/src/tooling/proxy-executor.d.ts +86 -0
- package/dist/src/tooling/proxy-executor.d.ts.map +1 -0
- package/dist/src/tooling/proxy-executor.js +501 -0
- package/dist/src/tooling/proxy-executor.js.map +1 -0
- package/dist/src/tooling/tool-generator.d.ts +67 -0
- package/dist/src/tooling/tool-generator.d.ts.map +1 -0
- package/dist/src/tooling/tool-generator.js +222 -0
- package/dist/src/tooling/tool-generator.js.map +1 -0
- package/dist/src/transport/http-client-factory.d.ts +65 -0
- package/dist/src/transport/http-client-factory.d.ts.map +1 -0
- package/dist/src/transport/http-client-factory.js +143 -0
- package/dist/src/transport/http-client-factory.js.map +1 -0
- package/dist/src/transport/http-transport-config.d.ts +6 -0
- package/dist/src/transport/http-transport-config.d.ts.map +1 -0
- package/dist/src/transport/http-transport-config.js +63 -0
- package/dist/src/transport/http-transport-config.js.map +1 -0
- package/dist/src/transport/http-transport.d.ts +329 -0
- package/dist/src/transport/http-transport.d.ts.map +1 -0
- package/dist/src/transport/http-transport.js +2584 -0
- package/dist/src/transport/http-transport.js.map +1 -0
- package/dist/src/transport/interceptors.d.ts +119 -0
- package/dist/src/transport/interceptors.d.ts.map +1 -0
- package/dist/src/transport/interceptors.js +413 -0
- package/dist/src/transport/interceptors.js.map +1 -0
- package/dist/src/transport/profile-index.d.ts +84 -0
- package/dist/src/transport/profile-index.d.ts.map +1 -0
- package/dist/src/transport/profile-index.js +405 -0
- package/dist/src/transport/profile-index.js.map +1 -0
- package/dist/src/types/http-transport.d.ts +26 -0
- package/dist/src/types/http-transport.d.ts.map +1 -1
- package/dist/src/types/openapi.d.ts +3 -0
- package/dist/src/types/openapi.d.ts.map +1 -1
- package/dist/src/types/profile.d.ts +16 -1
- package/dist/src/types/profile.d.ts.map +1 -1
- package/dist/src/validation/argument-normalizer.d.ts +6 -0
- package/dist/src/validation/argument-normalizer.d.ts.map +1 -0
- package/dist/src/validation/argument-normalizer.js +70 -0
- package/dist/src/validation/argument-normalizer.js.map +1 -0
- package/dist/src/validation/jsonrpc-validator.d.ts +27 -0
- package/dist/src/validation/jsonrpc-validator.d.ts.map +1 -0
- package/dist/src/validation/jsonrpc-validator.js +58 -0
- package/dist/src/validation/jsonrpc-validator.js.map +1 -0
- package/dist/src/validation/schema-validator.d.ts +30 -0
- package/dist/src/validation/schema-validator.d.ts.map +1 -0
- package/dist/src/validation/schema-validator.js +128 -0
- package/dist/src/validation/schema-validator.js.map +1 -0
- package/dist/src/validation/validation-utils.d.ts +49 -0
- package/dist/src/validation/validation-utils.d.ts.map +1 -0
- package/dist/src/validation/validation-utils.js +139 -0
- package/dist/src/validation/validation-utils.js.map +1 -0
- package/html/profile-index.html +386 -0
- package/package.json +10 -3
- package/profile-schema.json +77 -3
- package/profiles/gitlab/developer-profile-oauth.json +1520 -0
- package/profiles/gitlab/developer-profile-oauth.test.json +3432 -0
- package/profiles/gitlab/developer-profile.json +1508 -0
- package/profiles/gitlab/developer-profile.test.json +3432 -0
- package/profiles/gitlab/openapi.yaml +6891 -0
- package/profiles/n8n/openapi.yaml +2441 -0
- package/profiles/n8n/profile-optimized.json +965 -0
- package/profiles/n8n/profile-optimized.test.json +1078 -0
- package/profiles/n8n/profile.json +1033 -0
- package/profiles/n8n/profile.test.json +983 -0
- package/profiles/n8n-nodes/openapi.yaml +24 -0
- package/profiles/n8n-nodes/profile-nodes.json +44 -0
- package/profiles/n8n-nodes/profile-nodes.test.json +91 -0
- package/profiles/semgrep/openapi.yaml +4706 -0
- package/profiles/semgrep/profile.json +692 -0
- package/profiles/semgrep/profile.test.json +471 -0
- package/profiles/youtrack/openapi.json +16976 -0
- package/profiles/youtrack/profile.json +608 -0
- package/profiles/youtrack/profile.test.json +1926 -0
- package/dist/src/testing/fixtures.d.ts +0 -684
- package/dist/src/testing/fixtures.d.ts.map +0 -1
- package/dist/src/testing/fixtures.js +0 -528
- package/dist/src/testing/fixtures.js.map +0 -1
- package/dist/src/testing/mock-gitlab-server.d.ts +0 -43
- package/dist/src/testing/mock-gitlab-server.d.ts.map +0 -1
- package/dist/src/testing/mock-gitlab-server.js +0 -1026
- package/dist/src/testing/mock-gitlab-server.js.map +0 -1
- package/dist/src/testing/mock-semgrep-server.d.ts +0 -32
- package/dist/src/testing/mock-semgrep-server.d.ts.map +0 -1
- package/dist/src/testing/mock-semgrep-server.js +0 -213
- package/dist/src/testing/mock-semgrep-server.js.map +0 -1
- package/dist/src/testing/mock-youtrack-server.d.ts +0 -11
- package/dist/src/testing/mock-youtrack-server.d.ts.map +0 -1
- package/dist/src/testing/mock-youtrack-server.js +0 -152
- package/dist/src/testing/mock-youtrack-server.js.map +0 -1
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Provider Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements MCP SDK OAuthServerProvider interface to integrate with external
|
|
5
|
+
* OAuth 2.0 authorization servers (e.g., GitLab, GitHub, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - This server acts as an OAuth client to the external provider (Proxy/Gateway)
|
|
9
|
+
* - Implements "Callback Mode":
|
|
10
|
+
* 1. Client -> MCP (Authorize) -> MCP redirects to Provider (with MCP callback URL)
|
|
11
|
+
* 2. Provider -> MCP (Callback) -> MCP exchanges code for tokens
|
|
12
|
+
* 3. MCP redirects to Client (with Internal Code)
|
|
13
|
+
* 4. Client -> MCP (Token) -> MCP returns stored tokens
|
|
14
|
+
*/
|
|
15
|
+
import { randomUUID, createHash, timingSafeEqual } from 'node:crypto';
|
|
16
|
+
import { isIP } from 'node:net';
|
|
17
|
+
import { OAUTH_PATHS, OAUTH_RATE_LIMIT } from '../core/constants.js';
|
|
18
|
+
import { escapeHtmlSafe } from '../validation/validation-utils.js';
|
|
19
|
+
/**
|
|
20
|
+
* In-memory store for OAuth client registrations
|
|
21
|
+
*/
|
|
22
|
+
export class InMemoryClientsStore {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.clients = new Map();
|
|
25
|
+
}
|
|
26
|
+
async getClient(clientId) {
|
|
27
|
+
return this.clients.get(clientId);
|
|
28
|
+
}
|
|
29
|
+
async registerClient(clientMetadata) {
|
|
30
|
+
this.clients.set(clientMetadata.client_id, clientMetadata);
|
|
31
|
+
return clientMetadata;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* OAuth Provider Adapter for external OAuth servers
|
|
36
|
+
*/
|
|
37
|
+
export class ExternalOAuthProvider {
|
|
38
|
+
constructor(config, logger) {
|
|
39
|
+
// In-memory storage
|
|
40
|
+
this.authorizationCodes = new Map();
|
|
41
|
+
this.accessTokens = new Map();
|
|
42
|
+
this.stateStore = new Map();
|
|
43
|
+
this.endpointsInitialized = false;
|
|
44
|
+
this.initializationPromise = null;
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.logger = logger;
|
|
47
|
+
this._clientsStore = new InMemoryClientsStore();
|
|
48
|
+
// Resolve environment variables in OAuth config
|
|
49
|
+
this.config = this.resolveEnvVars(config);
|
|
50
|
+
// Pre-register mcp-proxy-client for VS Code compatibility
|
|
51
|
+
// VS Code doesn't call /oauth/register endpoint before calling /oauth/authorize
|
|
52
|
+
// This client has empty redirect_uris, allowing any redirect URI (validated at runtime)
|
|
53
|
+
const proxyClient = {
|
|
54
|
+
client_id: 'mcp-proxy-client',
|
|
55
|
+
client_secret: 'mcp-proxy-secret',
|
|
56
|
+
redirect_uris: [], // Empty = allow any redirect URI
|
|
57
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
58
|
+
response_types: ['code'],
|
|
59
|
+
scope: this.config.scopes ? this.config.scopes.join(' ') : '',
|
|
60
|
+
};
|
|
61
|
+
this._clientsStore.registerClient(proxyClient);
|
|
62
|
+
this.logger.info('Pre-registered mcp-proxy-client for VS Code compatibility');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Lazy initialization of OAuth endpoints (async)
|
|
66
|
+
* Public method to allow HttpTransport to ensure initialization before client validation
|
|
67
|
+
*/
|
|
68
|
+
async ensureEndpointsInitialized() {
|
|
69
|
+
if (this.endpointsInitialized) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Prevent concurrent initializations
|
|
73
|
+
if (this.initializationPromise) {
|
|
74
|
+
return this.initializationPromise;
|
|
75
|
+
}
|
|
76
|
+
this.initializationPromise = (async () => {
|
|
77
|
+
// Register default client BEFORE deriving endpoints
|
|
78
|
+
// This ensures the client is available immediately, even if endpoint derivation fails
|
|
79
|
+
// The client_id from config should be registered as soon as possible to avoid
|
|
80
|
+
// race conditions where /oauth/authorize is called before initialization completes
|
|
81
|
+
if (this.config.client_id) {
|
|
82
|
+
// Allow localhost and configured redirect_uri for default client
|
|
83
|
+
const allowedUris = [];
|
|
84
|
+
if (this.config.redirect_uri) {
|
|
85
|
+
allowedUris.push(this.config.redirect_uri);
|
|
86
|
+
}
|
|
87
|
+
// Also allow common localhost patterns for development/testing
|
|
88
|
+
allowedUris.push('http://localhost:3003/oauth/callback');
|
|
89
|
+
allowedUris.push('http://127.0.0.1:3003/oauth/callback');
|
|
90
|
+
const defaultClient = {
|
|
91
|
+
client_id: this.config.client_id,
|
|
92
|
+
client_secret: this.config.client_secret,
|
|
93
|
+
redirect_uris: allowedUris,
|
|
94
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
95
|
+
response_types: ['code'],
|
|
96
|
+
scope: this.config.scopes ? this.config.scopes.join(' ') : '',
|
|
97
|
+
};
|
|
98
|
+
this._clientsStore.registerClient(defaultClient);
|
|
99
|
+
this.logger.info('Registered default OAuth client', {
|
|
100
|
+
clientId: this.config.client_id,
|
|
101
|
+
redirectUris: allowedUris
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Derive endpoints from issuer if needed
|
|
105
|
+
this.config = await this.deriveEndpointsFromIssuer(this.config);
|
|
106
|
+
// Validate that we have required endpoints
|
|
107
|
+
if (!this.config.authorization_endpoint || !this.config.token_endpoint) {
|
|
108
|
+
throw new Error('OAuth config must provide either issuer OR both authorization_endpoint and token_endpoint');
|
|
109
|
+
}
|
|
110
|
+
this.logger.info('ExternalOAuthProvider initialized', {
|
|
111
|
+
authEndpoint: this.config.authorization_endpoint,
|
|
112
|
+
tokenEndpoint: this.config.token_endpoint,
|
|
113
|
+
hasClientId: !!this.config.client_id,
|
|
114
|
+
scopes: this.config.scopes || [],
|
|
115
|
+
});
|
|
116
|
+
this.endpointsInitialized = true;
|
|
117
|
+
})();
|
|
118
|
+
return this.initializationPromise;
|
|
119
|
+
}
|
|
120
|
+
get clientsStore() {
|
|
121
|
+
return this._clientsStore;
|
|
122
|
+
}
|
|
123
|
+
get authorizationEndpoint() {
|
|
124
|
+
// May be undefined before ensureEndpointsInitialized() is called
|
|
125
|
+
// when OAuth config provides issuer instead of explicit endpoints
|
|
126
|
+
return this.config.authorization_endpoint;
|
|
127
|
+
}
|
|
128
|
+
get redirectUri() {
|
|
129
|
+
return this.config.redirect_uri;
|
|
130
|
+
}
|
|
131
|
+
get scopes() {
|
|
132
|
+
return this.config.scopes || [];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Fetch OAuth Authorization Server Metadata (RFC 8414)
|
|
136
|
+
*/
|
|
137
|
+
async fetchOAuthMetadata(issuerUrl) {
|
|
138
|
+
try {
|
|
139
|
+
// Use URL constructor to properly handle trailing slashes
|
|
140
|
+
const metadataUrl = new URL(OAUTH_PATHS.WELL_KNOWN_AUTHORIZATION_SERVER, issuerUrl).toString();
|
|
141
|
+
const response = await fetch(metadataUrl, {
|
|
142
|
+
headers: { 'Accept': 'application/json' },
|
|
143
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const metadata = await response.json();
|
|
149
|
+
return {
|
|
150
|
+
authorization_endpoint: metadata.authorization_endpoint,
|
|
151
|
+
token_endpoint: metadata.token_endpoint,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
this.logger.debug('OAuth metadata fetch failed', { issuerUrl, error });
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Resolve environment variable references in OAuth config
|
|
161
|
+
*/
|
|
162
|
+
resolveEnvVars(config) {
|
|
163
|
+
const resolve = (value) => {
|
|
164
|
+
if (!value)
|
|
165
|
+
return value;
|
|
166
|
+
const match = value.match(/^\$\{env:([^}]+)\}$/);
|
|
167
|
+
if (match) {
|
|
168
|
+
const envVar = match[1];
|
|
169
|
+
const envValue = process.env[envVar];
|
|
170
|
+
if (!envValue) {
|
|
171
|
+
throw new Error(`Environment variable ${envVar} not found (referenced in OAuth config)`);
|
|
172
|
+
}
|
|
173
|
+
return envValue;
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
...config,
|
|
179
|
+
issuer: resolve(config.issuer),
|
|
180
|
+
authorization_endpoint: resolve(config.authorization_endpoint) || config.authorization_endpoint,
|
|
181
|
+
token_endpoint: resolve(config.token_endpoint) || config.token_endpoint,
|
|
182
|
+
client_id: resolve(config.client_id),
|
|
183
|
+
client_secret: resolve(config.client_secret),
|
|
184
|
+
redirect_uri: resolve(config.redirect_uri),
|
|
185
|
+
registration_endpoint: resolve(config.registration_endpoint),
|
|
186
|
+
introspection_endpoint: resolve(config.introspection_endpoint),
|
|
187
|
+
revocation_endpoint: resolve(config.revocation_endpoint),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Derive OAuth endpoints from issuer if needed
|
|
192
|
+
*/
|
|
193
|
+
async deriveEndpointsFromIssuer(config) {
|
|
194
|
+
// If both endpoints are explicitly provided, no need to derive
|
|
195
|
+
if (config.authorization_endpoint && config.token_endpoint) {
|
|
196
|
+
return config;
|
|
197
|
+
}
|
|
198
|
+
// If issuer is not provided, we can't derive
|
|
199
|
+
if (!config.issuer) {
|
|
200
|
+
if (!config.authorization_endpoint || !config.token_endpoint) {
|
|
201
|
+
throw new Error('OAuth config must provide either issuer OR both authorization_endpoint and token_endpoint');
|
|
202
|
+
}
|
|
203
|
+
return config;
|
|
204
|
+
}
|
|
205
|
+
const issuer = config.issuer;
|
|
206
|
+
this.logger.info('Deriving OAuth endpoints from issuer', { issuer });
|
|
207
|
+
// Try to fetch OAuth metadata
|
|
208
|
+
const metadata = await this.fetchOAuthMetadata(issuer);
|
|
209
|
+
if (metadata) {
|
|
210
|
+
this.logger.info('Successfully discovered OAuth endpoints', {
|
|
211
|
+
authorization_endpoint: metadata.authorization_endpoint,
|
|
212
|
+
token_endpoint: metadata.token_endpoint,
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
...config,
|
|
216
|
+
authorization_endpoint: config.authorization_endpoint || metadata.authorization_endpoint,
|
|
217
|
+
token_endpoint: config.token_endpoint || metadata.token_endpoint,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// Fallback to standard OAuth paths
|
|
221
|
+
this.logger.info('OAuth metadata fetch failed, using standard OAuth paths', { issuer });
|
|
222
|
+
return {
|
|
223
|
+
...config,
|
|
224
|
+
authorization_endpoint: config.authorization_endpoint || `${issuer}/oauth/authorize`,
|
|
225
|
+
token_endpoint: config.token_endpoint || `${issuer}/oauth/token`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if redirect URI host is allowed
|
|
230
|
+
* Prevents open redirect vulnerabilities (CWE-601)
|
|
231
|
+
*/
|
|
232
|
+
isAllowedRedirectHost(redirectUri) {
|
|
233
|
+
try {
|
|
234
|
+
const url = new URL(redirectUri);
|
|
235
|
+
const hostname = url.hostname;
|
|
236
|
+
// Default to localhost only if not configured
|
|
237
|
+
const allowedHosts = this.config.allowed_redirect_hosts || ['localhost', '127.0.0.1'];
|
|
238
|
+
for (const allowed of allowedHosts) {
|
|
239
|
+
if (this.matchRedirectHost(hostname, allowed)) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Invalid URL
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Match hostname against allowlist entry
|
|
252
|
+
*
|
|
253
|
+
* Supports:
|
|
254
|
+
* - Exact hostnames
|
|
255
|
+
* - Wildcard subdomains (*.example.com)
|
|
256
|
+
* - IPv4 exact matches
|
|
257
|
+
* - IPv4 CIDR ranges (e.g., 10.0.0.0/8)
|
|
258
|
+
* - IPv6 exact matches
|
|
259
|
+
* - IPv6 CIDR ranges (e.g., 2001:db8::/32)
|
|
260
|
+
*/
|
|
261
|
+
matchRedirectHost(hostname, pattern) {
|
|
262
|
+
const normalizedHost = this.stripIpv6Brackets(hostname);
|
|
263
|
+
const normalizedPattern = this.stripIpv6Brackets(pattern);
|
|
264
|
+
if (normalizedHost === normalizedPattern) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (normalizedPattern.startsWith('*.')) {
|
|
268
|
+
const domain = normalizedPattern.slice(2);
|
|
269
|
+
return normalizedHost === domain || normalizedHost.endsWith('.' + domain);
|
|
270
|
+
}
|
|
271
|
+
if (normalizedPattern.includes('/')) {
|
|
272
|
+
return this.matchCIDR(normalizedHost, normalizedPattern);
|
|
273
|
+
}
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check if IP address is within CIDR range
|
|
278
|
+
*
|
|
279
|
+
* Example: '192.168.1.50' matches '192.168.1.0/24'
|
|
280
|
+
* '2001:db8::1' matches '2001:db8::/32'
|
|
281
|
+
*/
|
|
282
|
+
matchCIDR(ip, cidr) {
|
|
283
|
+
const [rawRange, bits] = cidr.split('/');
|
|
284
|
+
const range = this.stripIpv6Brackets(rawRange);
|
|
285
|
+
const maskBits = parseInt(bits, 10);
|
|
286
|
+
if (isNaN(maskBits)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const ipVersion = isIP(ip);
|
|
290
|
+
const rangeVersion = isIP(range);
|
|
291
|
+
if (ipVersion === 0 || rangeVersion === 0 || ipVersion !== rangeVersion) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (ipVersion === 4) {
|
|
295
|
+
if (maskBits < 0 || maskBits > 32) {
|
|
296
|
+
this.logger.warn('Invalid IPv4 CIDR mask bits for redirect host allowlist', { cidr });
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const ipInt = this.ipv4ToInt(ip);
|
|
300
|
+
const rangeInt = this.ipv4ToInt(range);
|
|
301
|
+
if (ipInt === null || rangeInt === null) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const mask = (0xFFFFFFFF << (32 - maskBits)) >>> 0;
|
|
305
|
+
return (ipInt & mask) === (rangeInt & mask);
|
|
306
|
+
}
|
|
307
|
+
if (maskBits < 0 || maskBits > 128) {
|
|
308
|
+
this.logger.warn('Invalid IPv6 CIDR mask bits for redirect host allowlist', { cidr });
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const ipInt = this.ipv6ToBigInt(ip);
|
|
312
|
+
const rangeInt = this.ipv6ToBigInt(range);
|
|
313
|
+
if (ipInt === null || rangeInt === null) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
const mask = this.ipv6Mask(maskBits);
|
|
317
|
+
return (ipInt & mask) === (rangeInt & mask);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Convert IPv4 address to 32-bit integer
|
|
321
|
+
*/
|
|
322
|
+
ipv4ToInt(ip) {
|
|
323
|
+
const parts = ip.split('.');
|
|
324
|
+
if (parts.length !== 4) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
let result = 0;
|
|
328
|
+
for (let i = 0; i < 4; i++) {
|
|
329
|
+
const octet = parseInt(parts[i], 10);
|
|
330
|
+
if (isNaN(octet) || octet < 0 || octet > 255) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
result = (result << 8) | octet;
|
|
334
|
+
}
|
|
335
|
+
return result >>> 0;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Convert IPv6 address to 128-bit BigInt
|
|
339
|
+
*/
|
|
340
|
+
ipv6ToBigInt(ip) {
|
|
341
|
+
const cleaned = this.stripIpv6Brackets(ip);
|
|
342
|
+
// Handle IPv4-mapped IPv6 (e.g., ::ffff:192.168.0.1)
|
|
343
|
+
let ipv4Tail = null;
|
|
344
|
+
let base = cleaned;
|
|
345
|
+
if (cleaned.includes('.')) {
|
|
346
|
+
const lastColon = cleaned.lastIndexOf(':');
|
|
347
|
+
if (lastColon === -1)
|
|
348
|
+
return null;
|
|
349
|
+
const ipv4Part = cleaned.slice(lastColon + 1);
|
|
350
|
+
ipv4Tail = this.ipv4ToInt(ipv4Part);
|
|
351
|
+
if (ipv4Tail === null)
|
|
352
|
+
return null;
|
|
353
|
+
base = cleaned.slice(0, lastColon);
|
|
354
|
+
}
|
|
355
|
+
const parts = base.split('::');
|
|
356
|
+
if (parts.length > 2) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const head = parts[0] ? parts[0].split(':') : [];
|
|
360
|
+
const tail = parts.length === 2 && parts[1] ? parts[1].split(':') : [];
|
|
361
|
+
if (head.some(p => p === '') || tail.some(p => p === '')) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const totalSegmentsNeeded = 8 - (ipv4Tail !== null ? 2 : 0);
|
|
365
|
+
let segments = [];
|
|
366
|
+
const parseHextets = (items) => {
|
|
367
|
+
const result = [];
|
|
368
|
+
for (const part of items) {
|
|
369
|
+
const num = parseInt(part || '0', 16);
|
|
370
|
+
if (isNaN(num) || num < 0 || num > 0xFFFF) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
result.push(num);
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
const headVals = parseHextets(head);
|
|
378
|
+
const tailVals = parseHextets(tail);
|
|
379
|
+
if (!headVals || !tailVals) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const missing = totalSegmentsNeeded - (headVals.length + tailVals.length);
|
|
383
|
+
if (missing < 0) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
segments = [...headVals, ...Array(missing).fill(0), ...tailVals];
|
|
387
|
+
if (segments.length !== totalSegmentsNeeded) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (ipv4Tail !== null) {
|
|
391
|
+
const high = (ipv4Tail >>> 16) & 0xFFFF;
|
|
392
|
+
const low = ipv4Tail & 0xFFFF;
|
|
393
|
+
segments.push(high, low);
|
|
394
|
+
}
|
|
395
|
+
if (segments.length !== 8) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
let value = 0n;
|
|
399
|
+
for (const part of segments) {
|
|
400
|
+
value = (value << 16n) + BigInt(part);
|
|
401
|
+
}
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
ipv6Mask(maskBits) {
|
|
405
|
+
if (maskBits === 0) {
|
|
406
|
+
return 0n;
|
|
407
|
+
}
|
|
408
|
+
const ones = (1n << BigInt(maskBits)) - 1n;
|
|
409
|
+
return BigInt.asUintN(128, ones << BigInt(128 - maskBits));
|
|
410
|
+
}
|
|
411
|
+
stripIpv6Brackets(value) {
|
|
412
|
+
return value.replace(/^\[/, '').replace(/\]$/, '');
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Begin authorization flow
|
|
416
|
+
* Stores state and redirects to External Provider with MCP Callback URI
|
|
417
|
+
*/
|
|
418
|
+
async authorize(client, params, res) {
|
|
419
|
+
await this.ensureEndpointsInitialized();
|
|
420
|
+
this.logger.info('Starting OAuth authorization', {
|
|
421
|
+
clientId: client.client_id,
|
|
422
|
+
scopes: params.scopes,
|
|
423
|
+
redirectUri: params.redirectUri,
|
|
424
|
+
registeredUris: client.redirect_uris,
|
|
425
|
+
});
|
|
426
|
+
// Validate redirect URI
|
|
427
|
+
if (client.redirect_uris && client.redirect_uris.length > 0 && !client.redirect_uris.includes(params.redirectUri)) {
|
|
428
|
+
this.logger.error('Invalid redirect URI', undefined, {
|
|
429
|
+
providedUri: params.redirectUri,
|
|
430
|
+
registeredUris: client.redirect_uris,
|
|
431
|
+
});
|
|
432
|
+
throw new Error('Unregistered redirect_uri');
|
|
433
|
+
}
|
|
434
|
+
// Validate redirect host against allowlist to prevent open redirect
|
|
435
|
+
if (!this.isAllowedRedirectHost(params.redirectUri)) {
|
|
436
|
+
this.logger.error('Redirect URI host not allowed', undefined, {
|
|
437
|
+
providedUri: params.redirectUri,
|
|
438
|
+
allowedHosts: this.config.allowed_redirect_hosts || ['localhost', '127.0.0.1'],
|
|
439
|
+
});
|
|
440
|
+
throw new Error('Redirect URI host not allowed');
|
|
441
|
+
}
|
|
442
|
+
const stateToken = randomUUID();
|
|
443
|
+
this.stateStore.set(stateToken, {
|
|
444
|
+
clientRedirectUri: params.redirectUri,
|
|
445
|
+
codeChallenge: params.codeChallenge,
|
|
446
|
+
originalState: params.state,
|
|
447
|
+
clientId: client.client_id,
|
|
448
|
+
scopes: params.scopes,
|
|
449
|
+
createdAt: Date.now(),
|
|
450
|
+
});
|
|
451
|
+
const authUrl = new URL(this.config.authorization_endpoint);
|
|
452
|
+
const clientId = this.config.client_id || client.client_id;
|
|
453
|
+
if (!this.config.redirect_uri) {
|
|
454
|
+
throw new Error('MCP4_OAUTH_REDIRECT_URI must be configured');
|
|
455
|
+
}
|
|
456
|
+
const callbackUri = this.config.redirect_uri;
|
|
457
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
458
|
+
authUrl.searchParams.set('redirect_uri', callbackUri);
|
|
459
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
460
|
+
authUrl.searchParams.set('state', stateToken);
|
|
461
|
+
if (params.scopes && params.scopes.length > 0) {
|
|
462
|
+
authUrl.searchParams.set('scope', params.scopes.join(' '));
|
|
463
|
+
}
|
|
464
|
+
else if (this.config.scopes && this.config.scopes.length > 0) {
|
|
465
|
+
authUrl.searchParams.set('scope', this.config.scopes.join(' '));
|
|
466
|
+
}
|
|
467
|
+
// NOTE: Do NOT forward PKCE parameters to external provider
|
|
468
|
+
// The MCP server acts as an OAuth proxy with client_secret (confidential client)
|
|
469
|
+
// PKCE is used only between Cursor <-> MCP, not between MCP <-> External Provider
|
|
470
|
+
// If we forwarded code_challenge, we would need code_verifier which only Cursor has
|
|
471
|
+
this.logger.info('Redirecting to external OAuth provider', {
|
|
472
|
+
authUrl: authUrl.toString(),
|
|
473
|
+
callbackUri,
|
|
474
|
+
stateToken,
|
|
475
|
+
hasClientSecret: !!this.config.client_secret,
|
|
476
|
+
});
|
|
477
|
+
res.redirect(authUrl.toString());
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Handle callback from External Provider
|
|
481
|
+
* Exchanges code for tokens and redirects to Client with Internal Code
|
|
482
|
+
*/
|
|
483
|
+
async handleCallback(req, res) {
|
|
484
|
+
await this.ensureEndpointsInitialized();
|
|
485
|
+
const { code, state, error } = req.query;
|
|
486
|
+
if (error) {
|
|
487
|
+
this.logger.error('OAuth callback error', undefined, { error, state });
|
|
488
|
+
// Sanitize error messages to prevent XSS
|
|
489
|
+
const safeError = escapeHtmlSafe(error);
|
|
490
|
+
res.status(400);
|
|
491
|
+
res.json({
|
|
492
|
+
error: safeError,
|
|
493
|
+
error_description: `Authorization failed: ${safeError}`
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!code || typeof code !== 'string') {
|
|
498
|
+
res.status(400).send('Missing authorization code');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!state || typeof state !== 'string') {
|
|
502
|
+
res.status(400).send('Missing state parameter');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const storedState = this.stateStore.get(state);
|
|
506
|
+
if (!storedState) {
|
|
507
|
+
res.status(400).send('Invalid or expired state');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Clean up state
|
|
511
|
+
this.stateStore.delete(state);
|
|
512
|
+
try {
|
|
513
|
+
// Exchange External Code for Tokens
|
|
514
|
+
const tokens = await this.exchangeCodeWithProvider(code, undefined, this.config.redirect_uri);
|
|
515
|
+
// Generate Internal Code
|
|
516
|
+
const internalCode = randomUUID();
|
|
517
|
+
// Store Internal Code -> Tokens mapping
|
|
518
|
+
const client = await this._clientsStore.getClient(storedState.clientId);
|
|
519
|
+
if (!client)
|
|
520
|
+
throw new Error('Client not found');
|
|
521
|
+
this.authorizationCodes.set(internalCode, {
|
|
522
|
+
client,
|
|
523
|
+
params: {
|
|
524
|
+
redirectUri: storedState.clientRedirectUri,
|
|
525
|
+
codeChallenge: storedState.codeChallenge,
|
|
526
|
+
scopes: storedState.scopes || [],
|
|
527
|
+
state: storedState.originalState
|
|
528
|
+
},
|
|
529
|
+
createdAt: Date.now(),
|
|
530
|
+
tokens
|
|
531
|
+
});
|
|
532
|
+
// Re-validate redirect URI host + registration before redirect (defense-in-depth)
|
|
533
|
+
if (!this.isAllowedRedirectHost(storedState.clientRedirectUri)) {
|
|
534
|
+
this.logger.error('Redirect URI host not allowed (callback)', undefined, {
|
|
535
|
+
storedUri: storedState.clientRedirectUri,
|
|
536
|
+
allowedHosts: this.config.allowed_redirect_hosts || ['localhost', '127.0.0.1'],
|
|
537
|
+
});
|
|
538
|
+
res.status(400).send('Redirect URI host not allowed');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (client.redirect_uris && client.redirect_uris.length > 0 && !client.redirect_uris.includes(storedState.clientRedirectUri)) {
|
|
542
|
+
this.logger.error('Stored redirect URI no longer registered', undefined, {
|
|
543
|
+
storedUri: storedState.clientRedirectUri,
|
|
544
|
+
registeredUris: client.redirect_uris,
|
|
545
|
+
});
|
|
546
|
+
res.status(400).send('Unregistered redirect_uri');
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Redirect to Client
|
|
550
|
+
let clientUrl;
|
|
551
|
+
try {
|
|
552
|
+
// Allow custom schemes (e.g., vscode://, cursor://) as long as the host was validated above
|
|
553
|
+
clientUrl = new URL(storedState.clientRedirectUri);
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
res.status(400).send('Invalid redirect URI');
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
clientUrl.searchParams.set('code', internalCode);
|
|
560
|
+
if (storedState.originalState) {
|
|
561
|
+
clientUrl.searchParams.set('state', storedState.originalState);
|
|
562
|
+
}
|
|
563
|
+
this.logger.info('Redirecting to client with internal code', {
|
|
564
|
+
clientUrl: clientUrl.toString(),
|
|
565
|
+
internalCode
|
|
566
|
+
});
|
|
567
|
+
// nosemgrep: javascript.express.open-redirect-deepsemgrep.open-redirect-deepsemgrep, javascript.express.web.tainted-redirect-express.tainted-redirect-express
|
|
568
|
+
res.redirect(clientUrl.toString());
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
this.logger.error('Callback handling failed', err);
|
|
572
|
+
res.status(500).send('Internal Server Error during token exchange');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get code challenge for authorization code (Internal)
|
|
577
|
+
*/
|
|
578
|
+
async challengeForAuthorizationCode(client, authorizationCode) {
|
|
579
|
+
const codeData = this.authorizationCodes.get(authorizationCode);
|
|
580
|
+
if (!codeData) {
|
|
581
|
+
throw new Error('Invalid authorization code');
|
|
582
|
+
}
|
|
583
|
+
if (codeData.client.client_id !== client.client_id) {
|
|
584
|
+
throw new Error('Authorization code was not issued to this client');
|
|
585
|
+
}
|
|
586
|
+
return codeData.params.codeChallenge;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Exchange authorization code for access token (Internal)
|
|
590
|
+
*/
|
|
591
|
+
async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, redirectUri, resource) {
|
|
592
|
+
await this.ensureEndpointsInitialized();
|
|
593
|
+
this.logger.info('Exchanging internal authorization code', {
|
|
594
|
+
clientId: client.client_id,
|
|
595
|
+
});
|
|
596
|
+
const codeData = this.authorizationCodes.get(authorizationCode);
|
|
597
|
+
if (!codeData) {
|
|
598
|
+
throw new Error('Invalid authorization code');
|
|
599
|
+
}
|
|
600
|
+
if (codeData.client.client_id !== client.client_id) {
|
|
601
|
+
throw new Error('Authorization code was not issued to this client');
|
|
602
|
+
}
|
|
603
|
+
// Validate expiration (5 minutes)
|
|
604
|
+
const codeAge = Date.now() - codeData.createdAt;
|
|
605
|
+
const EXPIRATION_MS = 5 * 60 * 1000; // 5 minutes
|
|
606
|
+
if (codeAge > EXPIRATION_MS) {
|
|
607
|
+
this.authorizationCodes.delete(authorizationCode);
|
|
608
|
+
throw new Error('Authorization code expired');
|
|
609
|
+
}
|
|
610
|
+
// Validate PKCE if code challenge was provided
|
|
611
|
+
if (codeData.params.codeChallenge) {
|
|
612
|
+
if (!codeVerifier) {
|
|
613
|
+
throw new Error('code_verifier is required for PKCE');
|
|
614
|
+
}
|
|
615
|
+
// Verify code challenge (S256 method)
|
|
616
|
+
const hash = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
617
|
+
// Use constant-time comparison to prevent timing attacks
|
|
618
|
+
const hashBuffer = Buffer.from(hash);
|
|
619
|
+
const challengeBuffer = Buffer.from(codeData.params.codeChallenge);
|
|
620
|
+
if (hashBuffer.length !== challengeBuffer.length || !timingSafeEqual(hashBuffer, challengeBuffer)) {
|
|
621
|
+
throw new Error('Invalid code_verifier');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (!codeData.tokens) {
|
|
625
|
+
throw new Error('No tokens associated with this code');
|
|
626
|
+
}
|
|
627
|
+
// Delete authorization code (single use)
|
|
628
|
+
this.authorizationCodes.delete(authorizationCode);
|
|
629
|
+
// Store access token for validation
|
|
630
|
+
const tokenData = {
|
|
631
|
+
token: codeData.tokens.access_token,
|
|
632
|
+
clientId: client.client_id,
|
|
633
|
+
scopes: codeData.params.scopes || this.config.scopes || [],
|
|
634
|
+
expiresAt: codeData.tokens.expires_in
|
|
635
|
+
? Date.now() + codeData.tokens.expires_in * 1000
|
|
636
|
+
: undefined,
|
|
637
|
+
resource,
|
|
638
|
+
};
|
|
639
|
+
this.accessTokens.set(codeData.tokens.access_token, tokenData);
|
|
640
|
+
return codeData.tokens;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Exchange authorization code with external OAuth provider
|
|
644
|
+
*/
|
|
645
|
+
async exchangeCodeWithProvider(code, codeVerifier, redirectUri) {
|
|
646
|
+
const tokenUrl = this.config.token_endpoint;
|
|
647
|
+
const body = new URLSearchParams({
|
|
648
|
+
grant_type: 'authorization_code',
|
|
649
|
+
code,
|
|
650
|
+
redirect_uri: redirectUri,
|
|
651
|
+
});
|
|
652
|
+
if (codeVerifier) {
|
|
653
|
+
body.set('code_verifier', codeVerifier);
|
|
654
|
+
}
|
|
655
|
+
if (this.config.client_id) {
|
|
656
|
+
body.set('client_id', this.config.client_id);
|
|
657
|
+
}
|
|
658
|
+
if (this.config.client_secret) {
|
|
659
|
+
body.set('client_secret', this.config.client_secret);
|
|
660
|
+
}
|
|
661
|
+
this.logger.debug('Exchanging code with external provider', { tokenUrl });
|
|
662
|
+
const response = await fetch(tokenUrl, {
|
|
663
|
+
method: 'POST',
|
|
664
|
+
headers: {
|
|
665
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
666
|
+
'Accept': 'application/json',
|
|
667
|
+
},
|
|
668
|
+
body: body.toString(),
|
|
669
|
+
});
|
|
670
|
+
if (!response.ok) {
|
|
671
|
+
const errorText = await response.text();
|
|
672
|
+
this.logger.error('Token exchange failed', undefined, {
|
|
673
|
+
httpStatus: response.status,
|
|
674
|
+
errorMessage: errorText,
|
|
675
|
+
});
|
|
676
|
+
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
677
|
+
}
|
|
678
|
+
const tokenResponse = await response.json();
|
|
679
|
+
return tokenResponse;
|
|
680
|
+
}
|
|
681
|
+
async exchangeRefreshToken(client, refreshToken, scopes, resource) {
|
|
682
|
+
await this.ensureEndpointsInitialized();
|
|
683
|
+
this.logger.info('Exchanging refresh token', { clientId: client.client_id });
|
|
684
|
+
const tokenUrl = this.config.token_endpoint;
|
|
685
|
+
const body = new URLSearchParams({
|
|
686
|
+
grant_type: 'refresh_token',
|
|
687
|
+
refresh_token: refreshToken,
|
|
688
|
+
});
|
|
689
|
+
if (scopes && scopes.length > 0) {
|
|
690
|
+
body.set('scope', scopes.join(' '));
|
|
691
|
+
}
|
|
692
|
+
if (this.config.client_id) {
|
|
693
|
+
body.set('client_id', this.config.client_id);
|
|
694
|
+
}
|
|
695
|
+
if (this.config.client_secret) {
|
|
696
|
+
body.set('client_secret', this.config.client_secret);
|
|
697
|
+
}
|
|
698
|
+
const response = await fetch(tokenUrl, {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
headers: {
|
|
701
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
702
|
+
'Accept': 'application/json',
|
|
703
|
+
},
|
|
704
|
+
body: body.toString(),
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
const errorText = await response.text();
|
|
708
|
+
this.logger.error('Refresh token exchange failed', undefined, {
|
|
709
|
+
httpStatus: response.status,
|
|
710
|
+
errorMessage: errorText,
|
|
711
|
+
});
|
|
712
|
+
throw new Error(`Refresh token exchange failed: ${response.status}`);
|
|
713
|
+
}
|
|
714
|
+
const tokenResponse = await response.json();
|
|
715
|
+
const tokenData = {
|
|
716
|
+
token: tokenResponse.access_token,
|
|
717
|
+
clientId: client.client_id,
|
|
718
|
+
scopes: scopes || this.config.scopes || [],
|
|
719
|
+
expiresAt: tokenResponse.expires_in
|
|
720
|
+
? Date.now() + tokenResponse.expires_in * 1000
|
|
721
|
+
: undefined,
|
|
722
|
+
resource,
|
|
723
|
+
};
|
|
724
|
+
this.accessTokens.set(tokenResponse.access_token, tokenData);
|
|
725
|
+
return tokenResponse;
|
|
726
|
+
}
|
|
727
|
+
async verifyAccessToken(token) {
|
|
728
|
+
const tokenData = this.accessTokens.get(token);
|
|
729
|
+
if (!tokenData) {
|
|
730
|
+
if (this.config.introspection_endpoint) {
|
|
731
|
+
return await this.introspectToken(token);
|
|
732
|
+
}
|
|
733
|
+
throw new Error('Invalid or expired token');
|
|
734
|
+
}
|
|
735
|
+
if (tokenData.expiresAt && tokenData.expiresAt < Date.now()) {
|
|
736
|
+
this.accessTokens.delete(token);
|
|
737
|
+
throw new Error('Token expired');
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
token,
|
|
741
|
+
clientId: tokenData.clientId,
|
|
742
|
+
scopes: tokenData.scopes,
|
|
743
|
+
expiresAt: tokenData.expiresAt ? Math.floor(tokenData.expiresAt / 1000) : undefined,
|
|
744
|
+
resource: tokenData.resource,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
async introspectToken(token) {
|
|
748
|
+
const introspectionUrl = this.config.introspection_endpoint;
|
|
749
|
+
if (!introspectionUrl) {
|
|
750
|
+
throw new Error('Introspection endpoint not configured');
|
|
751
|
+
}
|
|
752
|
+
const body = new URLSearchParams({ token });
|
|
753
|
+
if (this.config.client_id) {
|
|
754
|
+
body.set('client_id', this.config.client_id);
|
|
755
|
+
}
|
|
756
|
+
if (this.config.client_secret) {
|
|
757
|
+
body.set('client_secret', this.config.client_secret);
|
|
758
|
+
}
|
|
759
|
+
const response = await fetch(introspectionUrl, {
|
|
760
|
+
method: 'POST',
|
|
761
|
+
headers: {
|
|
762
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
763
|
+
'Accept': 'application/json',
|
|
764
|
+
},
|
|
765
|
+
body: body.toString(),
|
|
766
|
+
});
|
|
767
|
+
if (!response.ok) {
|
|
768
|
+
throw new Error(`Token introspection failed: ${response.status}`);
|
|
769
|
+
}
|
|
770
|
+
const introspectionResponse = await response.json();
|
|
771
|
+
if (!introspectionResponse.active) {
|
|
772
|
+
throw new Error('Token is not active');
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
token,
|
|
776
|
+
clientId: introspectionResponse.client_id || 'unknown',
|
|
777
|
+
scopes: introspectionResponse.scope ? introspectionResponse.scope.split(' ') : [],
|
|
778
|
+
expiresAt: introspectionResponse.exp,
|
|
779
|
+
resource: introspectionResponse.aud ? new URL(introspectionResponse.aud) : undefined,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
async revokeToken(client, request) {
|
|
783
|
+
this.logger.info('Revoking token', { clientId: client.client_id });
|
|
784
|
+
this.accessTokens.delete(request.token);
|
|
785
|
+
if (this.config.revocation_endpoint) {
|
|
786
|
+
await this.revokeTokenWithProvider(request.token);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async revokeTokenWithProvider(token) {
|
|
790
|
+
const revocationUrl = this.config.revocation_endpoint;
|
|
791
|
+
if (!revocationUrl)
|
|
792
|
+
return;
|
|
793
|
+
const body = new URLSearchParams({ token });
|
|
794
|
+
if (this.config.client_id) {
|
|
795
|
+
body.set('client_id', this.config.client_id);
|
|
796
|
+
}
|
|
797
|
+
if (this.config.client_secret) {
|
|
798
|
+
body.set('client_secret', this.config.client_secret);
|
|
799
|
+
}
|
|
800
|
+
const response = await fetch(revocationUrl, {
|
|
801
|
+
method: 'POST',
|
|
802
|
+
headers: {
|
|
803
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
804
|
+
},
|
|
805
|
+
body: body.toString(),
|
|
806
|
+
});
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
this.logger.warn('Token revocation failed', { status: response.status });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Cleanup expired states, codes, and tokens
|
|
813
|
+
* Called periodically by HttpTransport
|
|
814
|
+
*/
|
|
815
|
+
cleanup() {
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
// 1. Cleanup expired states (10 minutes)
|
|
818
|
+
const STATE_TIMEOUT = OAUTH_RATE_LIMIT.WINDOW_MS;
|
|
819
|
+
for (const [state, data] of this.stateStore.entries()) {
|
|
820
|
+
if (now - data.createdAt > STATE_TIMEOUT) {
|
|
821
|
+
this.stateStore.delete(state);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// 2. Cleanup expired authorization codes (5 minutes)
|
|
825
|
+
const CODE_TIMEOUT = 5 * 60 * 1000;
|
|
826
|
+
for (const [code, data] of this.authorizationCodes.entries()) {
|
|
827
|
+
if (now - data.createdAt > CODE_TIMEOUT) {
|
|
828
|
+
this.authorizationCodes.delete(code);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// 3. Cleanup expired access tokens
|
|
832
|
+
for (const [token, data] of this.accessTokens.entries()) {
|
|
833
|
+
if (data.expiresAt && data.expiresAt < now) {
|
|
834
|
+
this.accessTokens.delete(token);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=oauth-provider.js.map
|