@memberjunction/server 3.4.0 → 4.1.0
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 +689 -513
- package/dist/agents/skip-agent.d.ts +65 -0
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +63 -5
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +163 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +143 -12
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/index.d.ts +0 -1
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.d.ts +82 -0
- package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -1
- package/dist/auth/APIKeyScopeAuth.js +78 -0
- package/dist/auth/APIKeyScopeAuth.js.map +1 -1
- package/dist/auth/AuthProviderFactory.d.ts +35 -0
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
- package/dist/auth/AuthProviderFactory.js +51 -4
- package/dist/auth/AuthProviderFactory.js.map +1 -1
- package/dist/auth/BaseAuthProvider.d.ts +21 -0
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +24 -9
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +32 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts +5 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +21 -6
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +35 -22
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +3 -0
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +6 -0
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +14 -3
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts +9 -0
- package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
- package/dist/auth/providers/Auth0Provider.js +10 -0
- package/dist/auth/providers/Auth0Provider.js.map +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts +9 -0
- package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
- package/dist/auth/providers/CognitoProvider.js +10 -0
- package/dist/auth/providers/CognitoProvider.js.map +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts +9 -0
- package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
- package/dist/auth/providers/GoogleProvider.js +11 -1
- package/dist/auth/providers/GoogleProvider.js.map +1 -1
- package/dist/auth/providers/MSALProvider.d.ts +9 -0
- package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
- package/dist/auth/providers/MSALProvider.js +10 -0
- package/dist/auth/providers/MSALProvider.js.map +1 -1
- package/dist/auth/providers/OktaProvider.d.ts +9 -0
- package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
- package/dist/auth/providers/OktaProvider.js +10 -0
- package/dist/auth/providers/OktaProvider.js.map +1 -1
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +42 -8
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +26 -4
- package/dist/context.js.map +1 -1
- package/dist/directives/Public.js +2 -0
- package/dist/directives/Public.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +7 -2
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +26 -8
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +539 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +9985 -14951
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/DeleteOptionsInput.d.ts +3 -0
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -1
- package/dist/generic/DeleteOptionsInput.js +3 -2
- package/dist/generic/DeleteOptionsInput.js.map +1 -1
- package/dist/generic/KeyInputOutputTypes.js +0 -6
- package/dist/generic/KeyInputOutputTypes.js.map +1 -1
- package/dist/generic/KeyValuePairInput.d.ts +4 -0
- package/dist/generic/KeyValuePairInput.d.ts.map +1 -1
- package/dist/generic/KeyValuePairInput.js +4 -2
- package/dist/generic/KeyValuePairInput.js.map +1 -1
- package/dist/generic/PushStatusResolver.js +0 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +58 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +203 -18
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +22 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +42 -108
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -37
- package/dist/index.js.map +1 -1
- package/dist/orm.d.ts.map +1 -1
- package/dist/orm.js +2 -1
- package/dist/orm.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts +74 -0
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -1
- package/dist/resolvers/APIKeyResolver.js +49 -10
- package/dist/resolvers/APIKeyResolver.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +189 -0
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +152 -21
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ColorResolver.js +0 -5
- package/dist/resolvers/ColorResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts +65 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +118 -40
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +47 -0
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +92 -116
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.js +2 -14
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +40 -0
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +2 -36
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +0 -7
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +15 -3
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +16 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +59 -74
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +18 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +17 -9
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts +19 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +35 -35
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.d.ts +2 -2
- package/dist/resolvers/InfoResolver.d.ts.map +1 -1
- package/dist/resolvers/InfoResolver.js +17 -20
- package/dist/resolvers/InfoResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +325 -1
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +931 -24
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +3 -29
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +0 -3
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +20 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +44 -36
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +3 -0
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +9 -10
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +54 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +116 -40
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts +42 -0
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +95 -22
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +9 -6
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts +12 -0
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +35 -21
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts +312 -0
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +295 -45
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +21 -0
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +36 -22
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +14 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +54 -21
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts +13 -0
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +22 -7
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/TelemetryResolver.d.ts +22 -0
- package/dist/resolvers/TelemetryResolver.d.ts.map +1 -1
- package/dist/resolvers/TelemetryResolver.js +45 -79
- package/dist/resolvers/TelemetryResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +11 -13
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +3 -12
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.js +10 -0
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.js +4 -0
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts +39 -0
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -0
- package/dist/resolvers/VersionHistoryResolver.js +208 -0
- package/dist/resolvers/VersionHistoryResolver.js.map +1 -0
- package/dist/rest/EntityCRUDHandler.d.ts +19 -0
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +55 -0
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +143 -0
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -0
- package/dist/rest/OAuthCallbackHandler.js +634 -0
- package/dist/rest/OAuthCallbackHandler.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +120 -0
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +213 -24
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +19 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +39 -0
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/index.d.ts +1 -0
- package/dist/rest/index.d.ts.map +1 -1
- package/dist/rest/index.js +1 -0
- package/dist/rest/index.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +35 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +15 -1
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts +31 -0
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +38 -4
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +73 -0
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +137 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -13
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +37 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +55 -8
- package/dist/util.js.map +1 -1
- package/package.json +83 -78
- package/src/auth/exampleNewUserSubClass.ts +1 -5
- package/src/auth/newUsers.ts +4 -2
- package/src/entitySubclasses/entityPermissions.server.ts +1 -3
- package/src/generated/generated.ts +4707 -2664
- package/src/index.ts +73 -62
- package/src/resolvers/FileCategoryResolver.ts +1 -1
- package/src/resolvers/InfoResolver.ts +10 -6
- package/src/resolvers/MCPResolver.ts +910 -10
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +0 -4
- package/src/resolvers/VersionHistoryResolver.ts +177 -0
- package/src/rest/OAuthCallbackHandler.ts +766 -0
- package/src/rest/RESTEndpointHandler.ts +58 -35
- package/src/rest/index.ts +2 -1
- package/src/rest/setupRESTEndpoints.ts +13 -12
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview OAuth Callback Handler for MCP OAuth flows
|
|
3
|
+
*
|
|
4
|
+
* Handles OAuth authorization callbacks from external authorization servers.
|
|
5
|
+
* This endpoint is unauthenticated since it's called by the auth server after user consent.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* - GET /api/v1/oauth/callback - Authorization callback
|
|
9
|
+
* - GET /api/v1/oauth/status/:stateParameter - Poll for authorization status (authenticated)
|
|
10
|
+
* - POST /api/v1/oauth/initiate - Initiate OAuth flow (authenticated)
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/server/rest/OAuthCallbackHandler
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { LogError, LogStatus, RunView, UserInfo } from '@memberjunction/core';
|
|
17
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
18
|
+
import { OAuthManager, MCPClientManager } from '@memberjunction/ai-mcp-client';
|
|
19
|
+
import type { MCPServerOAuthConfig } from '@memberjunction/ai-mcp-client';
|
|
20
|
+
|
|
21
|
+
/** Entity name for MCP Server Connections */
|
|
22
|
+
const ENTITY_MCP_SERVER_CONNECTIONS = 'MJ: MCP Server Connections';
|
|
23
|
+
|
|
24
|
+
/** Entity name for MCP Servers */
|
|
25
|
+
const ENTITY_MCP_SERVERS = 'MJ: MCP Servers';
|
|
26
|
+
|
|
27
|
+
/** Entity name for OAuth Authorization States */
|
|
28
|
+
const ENTITY_OAUTH_AUTHORIZATION_STATES = 'MJ: O Auth Authorization States';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Configuration for OAuth callback handler
|
|
32
|
+
*/
|
|
33
|
+
export interface OAuthCallbackHandlerOptions {
|
|
34
|
+
/** Base URL for this MJAPI instance (for redirects) */
|
|
35
|
+
publicUrl: string;
|
|
36
|
+
/** URL to redirect to after successful authorization */
|
|
37
|
+
successRedirectUrl?: string;
|
|
38
|
+
/** URL to redirect to after failed authorization */
|
|
39
|
+
errorRedirectUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handles OAuth callbacks and related endpoints for MCP server authentication.
|
|
44
|
+
*
|
|
45
|
+
* The callback endpoint is unauthenticated because it's called by external auth servers.
|
|
46
|
+
* It uses the state parameter to look up the authorization context and validate the flow.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const oauthHandler = new OAuthCallbackHandler({
|
|
51
|
+
* publicUrl: 'https://api.example.com'
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Mount unauthenticated callback route
|
|
55
|
+
* app.use('/api/v1/oauth', oauthHandler.getCallbackRouter());
|
|
56
|
+
*
|
|
57
|
+
* // Mount authenticated routes
|
|
58
|
+
* app.use('/api/v1/oauth', authMiddleware, oauthHandler.getAuthenticatedRouter());
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export class OAuthCallbackHandler {
|
|
62
|
+
private readonly options: OAuthCallbackHandlerOptions;
|
|
63
|
+
private readonly oauthManager: OAuthManager;
|
|
64
|
+
private readonly callbackRouter: express.Router;
|
|
65
|
+
private readonly authenticatedRouter: express.Router;
|
|
66
|
+
|
|
67
|
+
constructor(options: OAuthCallbackHandlerOptions) {
|
|
68
|
+
this.options = {
|
|
69
|
+
successRedirectUrl: `${options.publicUrl}/oauth/success`,
|
|
70
|
+
errorRedirectUrl: `${options.publicUrl}/oauth/error`,
|
|
71
|
+
...options
|
|
72
|
+
};
|
|
73
|
+
this.oauthManager = new OAuthManager();
|
|
74
|
+
this.callbackRouter = express.Router();
|
|
75
|
+
this.authenticatedRouter = express.Router();
|
|
76
|
+
this.setupRoutes();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sets up all routes for OAuth handling.
|
|
81
|
+
*/
|
|
82
|
+
private setupRoutes(): void {
|
|
83
|
+
// Unauthenticated callback route
|
|
84
|
+
this.callbackRouter.get('/callback', this.handleCallback.bind(this));
|
|
85
|
+
|
|
86
|
+
// Success and error pages (unauthenticated - user is redirected here after OAuth)
|
|
87
|
+
this.callbackRouter.get('/success', this.handleSuccessPage.bind(this));
|
|
88
|
+
this.callbackRouter.get('/error', this.handleErrorPage.bind(this));
|
|
89
|
+
|
|
90
|
+
// Authenticated routes
|
|
91
|
+
this.authenticatedRouter.get('/status/:stateParameter', this.getStatus.bind(this));
|
|
92
|
+
this.authenticatedRouter.post('/initiate', this.initiateFlow.bind(this));
|
|
93
|
+
this.authenticatedRouter.post('/exchange', this.handleExchange.bind(this));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Renders a success page after OAuth authorization completes.
|
|
98
|
+
*/
|
|
99
|
+
private handleSuccessPage(req: express.Request, res: express.Response): void {
|
|
100
|
+
const { state, connectionId } = req.query;
|
|
101
|
+
res.status(200).send(`
|
|
102
|
+
<!DOCTYPE html>
|
|
103
|
+
<html lang="en">
|
|
104
|
+
<head>
|
|
105
|
+
<meta charset="UTF-8">
|
|
106
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
107
|
+
<title>Authorization Successful</title>
|
|
108
|
+
<style>
|
|
109
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
110
|
+
.container { background: white; padding: 3rem; border-radius: 1rem; box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; }
|
|
111
|
+
.icon { font-size: 4rem; margin-bottom: 1rem; }
|
|
112
|
+
h1 { color: #22c55e; margin: 0 0 1rem; }
|
|
113
|
+
p { color: #64748b; margin: 0 0 1.5rem; line-height: 1.6; }
|
|
114
|
+
.details { background: #f1f5f9; padding: 1rem; border-radius: 0.5rem; font-size: 0.875rem; color: #475569; word-break: break-all; }
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="container">
|
|
119
|
+
<div class="icon">✅</div>
|
|
120
|
+
<h1>Authorization Successful</h1>
|
|
121
|
+
<p>Your MCP server connection has been authorized. You can close this window and return to MemberJunction.</p>
|
|
122
|
+
${connectionId ? `<div class="details">Connection ID: ${connectionId}</div>` : ''}
|
|
123
|
+
</div>
|
|
124
|
+
</body>
|
|
125
|
+
</html>
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Renders an error page when OAuth authorization fails.
|
|
131
|
+
*/
|
|
132
|
+
private handleErrorPage(req: express.Request, res: express.Response): void {
|
|
133
|
+
const { error, error_description } = req.query;
|
|
134
|
+
const errorCode = error ? String(error) : 'unknown_error';
|
|
135
|
+
const errorMessage = error_description ? String(error_description) : 'An error occurred during authorization.';
|
|
136
|
+
|
|
137
|
+
res.status(200).send(`
|
|
138
|
+
<!DOCTYPE html>
|
|
139
|
+
<html lang="en">
|
|
140
|
+
<head>
|
|
141
|
+
<meta charset="UTF-8">
|
|
142
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
143
|
+
<title>Authorization Failed</title>
|
|
144
|
+
<style>
|
|
145
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
146
|
+
.container { background: white; padding: 3rem; border-radius: 1rem; box-shadow: 0 20px 60px rgba(0,0,0,0.3); text-align: center; max-width: 400px; }
|
|
147
|
+
.icon { font-size: 4rem; margin-bottom: 1rem; }
|
|
148
|
+
h1 { color: #ef4444; margin: 0 0 1rem; }
|
|
149
|
+
p { color: #64748b; margin: 0 0 1.5rem; line-height: 1.6; }
|
|
150
|
+
.error-code { background: #fef2f2; color: #991b1b; padding: 0.5rem 1rem; border-radius: 0.5rem; font-family: monospace; display: inline-block; margin-bottom: 1rem; }
|
|
151
|
+
.error-message { background: #f1f5f9; padding: 1rem; border-radius: 0.5rem; font-size: 0.875rem; color: #475569; }
|
|
152
|
+
</style>
|
|
153
|
+
</head>
|
|
154
|
+
<body>
|
|
155
|
+
<div class="container">
|
|
156
|
+
<div class="icon">❌</div>
|
|
157
|
+
<h1>Authorization Failed</h1>
|
|
158
|
+
<div class="error-code">${errorCode}</div>
|
|
159
|
+
<div class="error-message">${errorMessage}</div>
|
|
160
|
+
<p style="margin-top: 1.5rem;">Please close this window and try again.</p>
|
|
161
|
+
</div>
|
|
162
|
+
</body>
|
|
163
|
+
</html>
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handles the OAuth authorization callback.
|
|
169
|
+
*
|
|
170
|
+
* This endpoint is unauthenticated - the auth server redirects here after user consent.
|
|
171
|
+
* We validate the state parameter and exchange the code for tokens.
|
|
172
|
+
*
|
|
173
|
+
* @param req - Express request
|
|
174
|
+
* @param res - Express response
|
|
175
|
+
*/
|
|
176
|
+
private async handleCallback(req: express.Request, res: express.Response): Promise<void> {
|
|
177
|
+
LogStatus(`[OAuth Callback] Received callback: ${req.originalUrl}`);
|
|
178
|
+
const { code, state, error, error_description } = req.query;
|
|
179
|
+
LogStatus(`[OAuth Callback] Parameters - state: ${state}, code: ${code ? 'present' : 'missing'}, error: ${error || 'none'}`);
|
|
180
|
+
|
|
181
|
+
// Validate state parameter is present
|
|
182
|
+
if (!state || typeof state !== 'string') {
|
|
183
|
+
this.redirectToError(res, 'invalid_request', 'Missing state parameter');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Get system user for initial lookup
|
|
189
|
+
const systemUser = this.getSystemUser();
|
|
190
|
+
if (!systemUser) {
|
|
191
|
+
LogError('[OAuth Callback] System user not available');
|
|
192
|
+
this.redirectToError(res, 'server_error', 'System configuration error');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Look up the authorization state to get the user context
|
|
197
|
+
const authState = await this.loadAuthorizationState(state, systemUser);
|
|
198
|
+
if (!authState) {
|
|
199
|
+
this.redirectToError(res, 'invalid_state', 'Authorization state not found or expired');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get the actual user from cache
|
|
204
|
+
const contextUser = UserCache.Users.find(u => u.ID === authState.userId);
|
|
205
|
+
if (!contextUser) {
|
|
206
|
+
LogError(`[OAuth Callback] User ${authState.userId} not found in cache`);
|
|
207
|
+
this.redirectToError(res, 'server_error', 'User context not found', authState.frontendReturnUrl);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle error from authorization server
|
|
212
|
+
if (error) {
|
|
213
|
+
const errorMessage = error_description ? String(error_description) : 'Authorization denied';
|
|
214
|
+
await this.oauthManager.handleAuthorizationError(
|
|
215
|
+
state,
|
|
216
|
+
String(error),
|
|
217
|
+
errorMessage,
|
|
218
|
+
contextUser
|
|
219
|
+
);
|
|
220
|
+
this.redirectToError(res, String(error), errorMessage, authState.frontendReturnUrl);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate authorization code is present
|
|
225
|
+
if (!code || typeof code !== 'string') {
|
|
226
|
+
this.redirectToError(res, 'invalid_request', 'Missing authorization code', authState.frontendReturnUrl);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Exchange code for tokens
|
|
231
|
+
const result = await this.oauthManager.completeAuthorizationFlow(state, code, contextUser);
|
|
232
|
+
|
|
233
|
+
if (result.success) {
|
|
234
|
+
LogStatus(`[OAuth Callback] Authorization completed for state ${state}`);
|
|
235
|
+
// Notify MCPClientManager that authorization has completed
|
|
236
|
+
MCPClientManager.Instance.notifyOAuthAuthorizationCompleted(authState.connectionId, {
|
|
237
|
+
stateParameter: state,
|
|
238
|
+
completedAt: new Date().toISOString()
|
|
239
|
+
});
|
|
240
|
+
this.redirectToSuccess(res, state, authState.connectionId, authState.frontendReturnUrl);
|
|
241
|
+
} else {
|
|
242
|
+
LogError(`[OAuth Callback] Authorization failed: ${result.errorMessage}`);
|
|
243
|
+
this.redirectToError(
|
|
244
|
+
res,
|
|
245
|
+
result.errorCode ?? 'authorization_failed',
|
|
246
|
+
result.errorMessage ?? 'Authorization failed',
|
|
247
|
+
authState.frontendReturnUrl
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
253
|
+
LogError(`[OAuth Callback] Unexpected error: ${errorMessage}`);
|
|
254
|
+
this.redirectToError(res, 'server_error', 'An unexpected error occurred');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gets the status of an OAuth authorization flow.
|
|
260
|
+
*
|
|
261
|
+
* Authenticated endpoint for polling authorization status.
|
|
262
|
+
*
|
|
263
|
+
* @param req - Express request
|
|
264
|
+
* @param res - Express response
|
|
265
|
+
*/
|
|
266
|
+
private async getStatus(req: express.Request, res: express.Response): Promise<void> {
|
|
267
|
+
const stateParam = req.params.stateParameter;
|
|
268
|
+
const stateParameter = Array.isArray(stateParam) ? stateParam[0] : stateParam || '';
|
|
269
|
+
const contextUser = req['mjUser'] as UserInfo;
|
|
270
|
+
|
|
271
|
+
if (!contextUser) {
|
|
272
|
+
res.status(401).json({
|
|
273
|
+
success: false,
|
|
274
|
+
errorMessage: 'Authentication required'
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const authState = await this.loadAuthorizationState(stateParameter, contextUser);
|
|
281
|
+
|
|
282
|
+
if (!authState) {
|
|
283
|
+
res.status(404).json({
|
|
284
|
+
success: false,
|
|
285
|
+
errorCode: 'not_found',
|
|
286
|
+
errorMessage: 'Authorization state not found'
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Verify user owns this state
|
|
292
|
+
if (authState.userId !== contextUser.ID) {
|
|
293
|
+
res.status(403).json({
|
|
294
|
+
success: false,
|
|
295
|
+
errorCode: 'forbidden',
|
|
296
|
+
errorMessage: 'Access denied'
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Map status
|
|
302
|
+
let status: 'pending' | 'completed' | 'failed' | 'expired';
|
|
303
|
+
switch (authState.status) {
|
|
304
|
+
case 'Pending':
|
|
305
|
+
status = new Date() >= authState.expiresAt ? 'expired' : 'pending';
|
|
306
|
+
break;
|
|
307
|
+
case 'Completed':
|
|
308
|
+
status = 'completed';
|
|
309
|
+
break;
|
|
310
|
+
case 'Failed':
|
|
311
|
+
status = 'failed';
|
|
312
|
+
break;
|
|
313
|
+
case 'Expired':
|
|
314
|
+
status = 'expired';
|
|
315
|
+
break;
|
|
316
|
+
default:
|
|
317
|
+
status = 'pending';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
res.json({
|
|
321
|
+
status,
|
|
322
|
+
connectionId: authState.connectionId,
|
|
323
|
+
completedAt: authState.completedAt?.toISOString(),
|
|
324
|
+
errorCode: authState.errorCode,
|
|
325
|
+
errorMessage: authState.errorDescription,
|
|
326
|
+
isRetryable: status === 'failed' || status === 'expired'
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
} catch (error) {
|
|
330
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
331
|
+
LogError(`[OAuth Status] Error: ${errorMessage}`);
|
|
332
|
+
res.status(500).json({
|
|
333
|
+
success: false,
|
|
334
|
+
errorCode: 'server_error',
|
|
335
|
+
errorMessage: 'Failed to get authorization status'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Initiates a new OAuth authorization flow.
|
|
342
|
+
*
|
|
343
|
+
* Authenticated endpoint for starting OAuth authorization.
|
|
344
|
+
*
|
|
345
|
+
* @param req - Express request
|
|
346
|
+
* @param res - Express response
|
|
347
|
+
*/
|
|
348
|
+
private async initiateFlow(req: express.Request, res: express.Response): Promise<void> {
|
|
349
|
+
const { connectionId, additionalScopes, frontendReturnUrl } = req.body;
|
|
350
|
+
const contextUser = req['mjUser'] as UserInfo;
|
|
351
|
+
|
|
352
|
+
if (!contextUser) {
|
|
353
|
+
res.status(401).json({
|
|
354
|
+
success: false,
|
|
355
|
+
errorMessage: 'Authentication required'
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!connectionId) {
|
|
361
|
+
res.status(400).json({
|
|
362
|
+
success: false,
|
|
363
|
+
errorCode: 'invalid_request',
|
|
364
|
+
errorMessage: 'connectionId is required'
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
// Load connection and server config
|
|
371
|
+
const config = await this.loadConnectionConfig(connectionId, contextUser);
|
|
372
|
+
if (!config) {
|
|
373
|
+
res.status(404).json({
|
|
374
|
+
success: false,
|
|
375
|
+
errorCode: 'not_found',
|
|
376
|
+
errorMessage: 'Connection not found'
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Build OAuth config
|
|
382
|
+
const oauthConfig: MCPServerOAuthConfig = {
|
|
383
|
+
OAuthIssuerURL: config.OAuthIssuerURL,
|
|
384
|
+
OAuthScopes: additionalScopes
|
|
385
|
+
? `${config.OAuthScopes ?? ''} ${additionalScopes}`.trim()
|
|
386
|
+
: config.OAuthScopes,
|
|
387
|
+
OAuthMetadataCacheTTLMinutes: config.OAuthMetadataCacheTTLMinutes,
|
|
388
|
+
OAuthClientID: config.OAuthClientID,
|
|
389
|
+
OAuthClientSecretEncrypted: config.OAuthClientSecretEncrypted,
|
|
390
|
+
OAuthRequirePKCE: config.OAuthRequirePKCE
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (!oauthConfig.OAuthIssuerURL) {
|
|
394
|
+
res.status(400).json({
|
|
395
|
+
success: false,
|
|
396
|
+
errorCode: 'invalid_configuration',
|
|
397
|
+
errorMessage: 'OAuth is not configured for this server'
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Initiate the flow with optional frontend return URL
|
|
403
|
+
const result = await this.oauthManager.initiateAuthorizationFlow(
|
|
404
|
+
connectionId,
|
|
405
|
+
config.serverId,
|
|
406
|
+
oauthConfig,
|
|
407
|
+
this.options.publicUrl,
|
|
408
|
+
contextUser,
|
|
409
|
+
frontendReturnUrl ? { frontendReturnUrl: String(frontendReturnUrl) } : undefined
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if (result.success) {
|
|
413
|
+
res.json({
|
|
414
|
+
success: true,
|
|
415
|
+
authorizationUrl: result.authorizationUrl,
|
|
416
|
+
stateParameter: result.stateParameter,
|
|
417
|
+
expiresAt: result.expiresAt?.toISOString(),
|
|
418
|
+
message: 'Please redirect the user to the authorization URL'
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
res.status(400).json({
|
|
422
|
+
success: false,
|
|
423
|
+
errorCode: 'initiation_failed',
|
|
424
|
+
errorMessage: result.errorMessage
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
} catch (error) {
|
|
429
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
430
|
+
LogError(`[OAuth Initiate] Error: ${errorMessage}`);
|
|
431
|
+
res.status(500).json({
|
|
432
|
+
success: false,
|
|
433
|
+
errorCode: 'server_error',
|
|
434
|
+
errorMessage: 'Failed to initiate OAuth flow'
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Exchanges an authorization code for tokens.
|
|
441
|
+
*
|
|
442
|
+
* This endpoint is used when the frontend handles the OAuth callback.
|
|
443
|
+
* The frontend receives the code and state from the OAuth provider redirect,
|
|
444
|
+
* then calls this authenticated endpoint to complete the token exchange.
|
|
445
|
+
*
|
|
446
|
+
* @param req - Express request with { code, state } in body
|
|
447
|
+
* @param res - Express response
|
|
448
|
+
*/
|
|
449
|
+
private async handleExchange(req: express.Request, res: express.Response): Promise<void> {
|
|
450
|
+
const { code, state } = req.body;
|
|
451
|
+
const contextUser = req['mjUser'] as UserInfo;
|
|
452
|
+
|
|
453
|
+
if (!contextUser) {
|
|
454
|
+
res.status(401).json({
|
|
455
|
+
success: false,
|
|
456
|
+
errorCode: 'unauthorized',
|
|
457
|
+
errorMessage: 'Authentication required'
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!code || typeof code !== 'string') {
|
|
463
|
+
res.status(400).json({
|
|
464
|
+
success: false,
|
|
465
|
+
errorCode: 'invalid_request',
|
|
466
|
+
errorMessage: 'Missing or invalid code parameter'
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!state || typeof state !== 'string') {
|
|
472
|
+
res.status(400).json({
|
|
473
|
+
success: false,
|
|
474
|
+
errorCode: 'invalid_request',
|
|
475
|
+
errorMessage: 'Missing or invalid state parameter'
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
// Load the authorization state to verify user ownership
|
|
482
|
+
const authState = await this.loadAuthorizationState(state, contextUser);
|
|
483
|
+
|
|
484
|
+
if (!authState) {
|
|
485
|
+
res.status(404).json({
|
|
486
|
+
success: false,
|
|
487
|
+
errorCode: 'state_not_found',
|
|
488
|
+
errorMessage: 'Authorization state not found or expired'
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Verify the authenticated user owns this authorization state
|
|
494
|
+
if (authState.userId !== contextUser.ID) {
|
|
495
|
+
LogError(`[OAuth Exchange] User ${contextUser.ID} attempted to exchange code for state owned by ${authState.userId}`);
|
|
496
|
+
res.status(403).json({
|
|
497
|
+
success: false,
|
|
498
|
+
errorCode: 'forbidden',
|
|
499
|
+
errorMessage: 'You are not authorized to complete this authorization flow'
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Exchange code for tokens using OAuthManager
|
|
505
|
+
const result = await this.oauthManager.completeAuthorizationFlow(state, code, contextUser);
|
|
506
|
+
|
|
507
|
+
if (result.success) {
|
|
508
|
+
LogStatus(`[OAuth Exchange] Successfully exchanged code for connection ${authState.connectionId}`);
|
|
509
|
+
|
|
510
|
+
// Notify MCPClientManager that authorization has completed
|
|
511
|
+
MCPClientManager.Instance.notifyOAuthAuthorizationCompleted(authState.connectionId, {
|
|
512
|
+
stateParameter: state,
|
|
513
|
+
completedAt: new Date().toISOString()
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
res.json({
|
|
517
|
+
success: true,
|
|
518
|
+
connectionId: authState.connectionId
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
LogError(`[OAuth Exchange] Token exchange failed: ${result.errorMessage}`);
|
|
522
|
+
res.status(400).json({
|
|
523
|
+
success: false,
|
|
524
|
+
errorCode: result.errorCode ?? 'exchange_failed',
|
|
525
|
+
errorMessage: result.errorMessage ?? 'Failed to exchange authorization code for tokens',
|
|
526
|
+
isRetryable: result.isRetryable ?? false
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
531
|
+
LogError(`[OAuth Exchange] Unexpected error: ${errorMessage}`);
|
|
532
|
+
res.status(500).json({
|
|
533
|
+
success: false,
|
|
534
|
+
errorCode: 'server_error',
|
|
535
|
+
errorMessage: 'An unexpected error occurred during token exchange'
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ========================================
|
|
541
|
+
// Helper Methods
|
|
542
|
+
// ========================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Gets the system user from cache.
|
|
546
|
+
*/
|
|
547
|
+
private getSystemUser(): UserInfo | null {
|
|
548
|
+
try {
|
|
549
|
+
return UserCache.Instance.GetSystemUser();
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Loads authorization state from database.
|
|
557
|
+
*/
|
|
558
|
+
private async loadAuthorizationState(
|
|
559
|
+
stateParameter: string,
|
|
560
|
+
contextUser: UserInfo
|
|
561
|
+
): Promise<{
|
|
562
|
+
connectionId: string;
|
|
563
|
+
userId: string;
|
|
564
|
+
status: string;
|
|
565
|
+
errorCode?: string;
|
|
566
|
+
errorDescription?: string;
|
|
567
|
+
expiresAt: Date;
|
|
568
|
+
completedAt?: Date;
|
|
569
|
+
frontendReturnUrl?: string;
|
|
570
|
+
} | null> {
|
|
571
|
+
try {
|
|
572
|
+
const rv = new RunView();
|
|
573
|
+
const result = await rv.RunView<{
|
|
574
|
+
MCPServerConnectionID: string;
|
|
575
|
+
UserID: string;
|
|
576
|
+
Status: string;
|
|
577
|
+
ErrorCode: string | null;
|
|
578
|
+
ErrorDescription: string | null;
|
|
579
|
+
ExpiresAt: Date;
|
|
580
|
+
CompletedAt: Date | null;
|
|
581
|
+
FrontendReturnURL: string | null;
|
|
582
|
+
}>({
|
|
583
|
+
EntityName: ENTITY_OAUTH_AUTHORIZATION_STATES,
|
|
584
|
+
ExtraFilter: `StateParameter='${stateParameter.replace(/'/g, "''")}'`,
|
|
585
|
+
Fields: ['MCPServerConnectionID', 'UserID', 'Status', 'ErrorCode', 'ErrorDescription', 'ExpiresAt', 'CompletedAt', 'FrontendReturnURL'],
|
|
586
|
+
ResultType: 'simple'
|
|
587
|
+
}, contextUser);
|
|
588
|
+
|
|
589
|
+
if (!result.Success || !result.Results || result.Results.length === 0) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const record = result.Results[0];
|
|
594
|
+
return {
|
|
595
|
+
connectionId: record.MCPServerConnectionID,
|
|
596
|
+
userId: record.UserID,
|
|
597
|
+
status: record.Status,
|
|
598
|
+
errorCode: record.ErrorCode ?? undefined,
|
|
599
|
+
errorDescription: record.ErrorDescription ?? undefined,
|
|
600
|
+
expiresAt: new Date(record.ExpiresAt),
|
|
601
|
+
completedAt: record.CompletedAt ? new Date(record.CompletedAt) : undefined,
|
|
602
|
+
frontendReturnUrl: record.FrontendReturnURL ?? undefined
|
|
603
|
+
};
|
|
604
|
+
} catch (error) {
|
|
605
|
+
LogError(`[OAuth Callback] Failed to load authorization state: ${error}`);
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Loads connection and server OAuth configuration.
|
|
612
|
+
*/
|
|
613
|
+
private async loadConnectionConfig(
|
|
614
|
+
connectionId: string,
|
|
615
|
+
contextUser: UserInfo
|
|
616
|
+
): Promise<{
|
|
617
|
+
serverId: string;
|
|
618
|
+
OAuthIssuerURL?: string;
|
|
619
|
+
OAuthScopes?: string;
|
|
620
|
+
OAuthMetadataCacheTTLMinutes?: number;
|
|
621
|
+
OAuthClientID?: string;
|
|
622
|
+
OAuthClientSecretEncrypted?: string;
|
|
623
|
+
OAuthRequirePKCE?: boolean;
|
|
624
|
+
} | null> {
|
|
625
|
+
try {
|
|
626
|
+
const rv = new RunView();
|
|
627
|
+
|
|
628
|
+
// Get connection to get server ID
|
|
629
|
+
const connResult = await rv.RunView<{ MCPServerID: string }>({
|
|
630
|
+
EntityName: ENTITY_MCP_SERVER_CONNECTIONS,
|
|
631
|
+
ExtraFilter: `ID='${connectionId}'`,
|
|
632
|
+
Fields: ['MCPServerID'],
|
|
633
|
+
ResultType: 'simple'
|
|
634
|
+
}, contextUser);
|
|
635
|
+
|
|
636
|
+
if (!connResult.Success || !connResult.Results || connResult.Results.length === 0) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const serverId = connResult.Results[0].MCPServerID;
|
|
641
|
+
|
|
642
|
+
// Get server OAuth config
|
|
643
|
+
const serverResult = await rv.RunView<{
|
|
644
|
+
OAuthIssuerURL: string | null;
|
|
645
|
+
OAuthScopes: string | null;
|
|
646
|
+
OAuthMetadataCacheTTLMinutes: number | null;
|
|
647
|
+
OAuthClientID: string | null;
|
|
648
|
+
OAuthClientSecretEncrypted: string | null;
|
|
649
|
+
OAuthRequirePKCE: boolean | null;
|
|
650
|
+
}>({
|
|
651
|
+
EntityName: ENTITY_MCP_SERVERS,
|
|
652
|
+
ExtraFilter: `ID='${serverId}'`,
|
|
653
|
+
Fields: [
|
|
654
|
+
'OAuthIssuerURL', 'OAuthScopes', 'OAuthMetadataCacheTTLMinutes',
|
|
655
|
+
'OAuthClientID', 'OAuthClientSecretEncrypted', 'OAuthRequirePKCE'
|
|
656
|
+
],
|
|
657
|
+
ResultType: 'simple'
|
|
658
|
+
}, contextUser);
|
|
659
|
+
|
|
660
|
+
if (!serverResult.Success || !serverResult.Results || serverResult.Results.length === 0) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const server = serverResult.Results[0];
|
|
665
|
+
return {
|
|
666
|
+
serverId,
|
|
667
|
+
OAuthIssuerURL: server.OAuthIssuerURL ?? undefined,
|
|
668
|
+
OAuthScopes: server.OAuthScopes ?? undefined,
|
|
669
|
+
OAuthMetadataCacheTTLMinutes: server.OAuthMetadataCacheTTLMinutes ?? undefined,
|
|
670
|
+
OAuthClientID: server.OAuthClientID ?? undefined,
|
|
671
|
+
OAuthClientSecretEncrypted: server.OAuthClientSecretEncrypted ?? undefined,
|
|
672
|
+
OAuthRequirePKCE: server.OAuthRequirePKCE ?? undefined
|
|
673
|
+
};
|
|
674
|
+
} catch (error) {
|
|
675
|
+
LogError(`[OAuth Callback] Failed to load connection config: ${error}`);
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Redirects to success page with state info.
|
|
682
|
+
* If a frontend return URL is provided, redirects there instead of the default success page.
|
|
683
|
+
*/
|
|
684
|
+
private redirectToSuccess(res: express.Response, state: string, connectionId: string, frontendReturnUrl?: string): void {
|
|
685
|
+
// If frontend return URL is provided, redirect there with success parameters
|
|
686
|
+
if (frontendReturnUrl) {
|
|
687
|
+
try {
|
|
688
|
+
const url = new URL(frontendReturnUrl);
|
|
689
|
+
url.searchParams.set('oauth', 'success');
|
|
690
|
+
url.searchParams.set('state', state);
|
|
691
|
+
url.searchParams.set('connectionId', connectionId);
|
|
692
|
+
LogStatus(`[OAuth Callback] Redirecting to frontend URL: ${url.toString()}`);
|
|
693
|
+
res.redirect(302, url.toString());
|
|
694
|
+
return;
|
|
695
|
+
} catch (error) {
|
|
696
|
+
LogError(`[OAuth Callback] Invalid frontend return URL '${frontendReturnUrl}', falling back to default`);
|
|
697
|
+
// Fall through to default redirect
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Default: redirect to built-in success page
|
|
702
|
+
const url = new URL(this.options.successRedirectUrl!);
|
|
703
|
+
url.searchParams.set('state', state);
|
|
704
|
+
url.searchParams.set('connectionId', connectionId);
|
|
705
|
+
res.redirect(302, url.toString());
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Redirects to error page with error info.
|
|
710
|
+
* If a frontend return URL is provided, redirects there instead of the default error page.
|
|
711
|
+
*/
|
|
712
|
+
private redirectToError(res: express.Response, errorCode: string, errorMessage: string, frontendReturnUrl?: string): void {
|
|
713
|
+
// If frontend return URL is provided, redirect there with error parameters
|
|
714
|
+
if (frontendReturnUrl) {
|
|
715
|
+
try {
|
|
716
|
+
const url = new URL(frontendReturnUrl);
|
|
717
|
+
url.searchParams.set('oauth', 'error');
|
|
718
|
+
url.searchParams.set('error', errorCode);
|
|
719
|
+
url.searchParams.set('error_description', errorMessage);
|
|
720
|
+
LogStatus(`[OAuth Callback] Redirecting to frontend URL with error: ${url.toString()}`);
|
|
721
|
+
res.redirect(302, url.toString());
|
|
722
|
+
return;
|
|
723
|
+
} catch (error) {
|
|
724
|
+
LogError(`[OAuth Callback] Invalid frontend return URL '${frontendReturnUrl}', falling back to default`);
|
|
725
|
+
// Fall through to default redirect
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Default: redirect to built-in error page
|
|
730
|
+
const url = new URL(this.options.errorRedirectUrl!);
|
|
731
|
+
url.searchParams.set('error', errorCode);
|
|
732
|
+
url.searchParams.set('error_description', errorMessage);
|
|
733
|
+
res.redirect(302, url.toString());
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Gets the router for unauthenticated callback endpoint.
|
|
738
|
+
*/
|
|
739
|
+
public getCallbackRouter(): express.Router {
|
|
740
|
+
return this.callbackRouter;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Gets the router for authenticated OAuth endpoints.
|
|
745
|
+
*/
|
|
746
|
+
public getAuthenticatedRouter(): express.Router {
|
|
747
|
+
return this.authenticatedRouter;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Creates and configures the OAuth callback handler.
|
|
753
|
+
*
|
|
754
|
+
* @param options - Handler configuration
|
|
755
|
+
* @returns Object with callback and authenticated routers
|
|
756
|
+
*/
|
|
757
|
+
export function createOAuthCallbackHandler(options: OAuthCallbackHandlerOptions): {
|
|
758
|
+
callbackRouter: express.Router;
|
|
759
|
+
authenticatedRouter: express.Router;
|
|
760
|
+
} {
|
|
761
|
+
const handler = new OAuthCallbackHandler(options);
|
|
762
|
+
return {
|
|
763
|
+
callbackRouter: handler.getCallbackRouter(),
|
|
764
|
+
authenticatedRouter: handler.getAuthenticatedRouter()
|
|
765
|
+
};
|
|
766
|
+
}
|