Line data Source code
1 : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
2 :
3 : import { dateToTimestamp } from '../proto_helper.js';
4 :
5 : import {
6 : SETUP_CANCELLED,
7 : SETUP_FAILED,
8 : SETUP_STARTED,
9 : GENERATED_PKCE_CHALLENGE,
10 : GENERATING_PKCE_CHALLENGE,
11 : OAUTH_NATIVE_LINK_OPENED,
12 : OAUTH_WEB_WINDOW_CLOSED,
13 : OAUTH_WEB_WINDOW_OPENED,
14 : RECEIVED_CORE_AUTH_CODE,
15 : RECEIVED_CORE_AUTH_TOKEN,
16 : RECEIVED_CORE_USER_INFO,
17 : RECEIVED_DATA_CLOUD_ACCESS_TOKEN,
18 : REQUESTING_CORE_AUTH_TOKEN,
19 : REQUESTING_DATA_CLOUD_ACCESS_TOKEN,
20 : SalesforceConnectionStateAction,
21 : SF_CHANNEL_READY,
22 : SF_CHANNEL_SETUP_STARTED,
23 : } from './salesforce_connection_state.js';
24 : import { PlatformType } from '../../platform/platform_type.js';
25 : import { SalesforceConnectorConfig } from '../connector_configs.js';
26 : import { collectSalesforceAuthInfo, SalesforceApiClientInterface, SalesforceDatabaseChannel } from './salesforce_api_client.js';
27 : import { Dispatch } from '../../utils/variant.js';
28 : import { Logger, stringifyError } from '../../platform/logger/logger.js';
29 : import { PlatformEventListener } from '../../platform/events/event_listener.js';
30 : import { RESET_CONNECTION } from '../connection_state.js';
31 : import { AttachedDatabase, HyperDatabaseChannel, HyperDatabaseClient, HyperDatabaseConnectionContext } from '../hyper/hyperdb_grpc_client.js';
32 : import { authenticateSalesforce, SalesforceAuthenticationProgress } from './salesforce_authentication.js';
33 : import { ConnectorType } from '../connector_info.js';
34 : import { getSidbAttachedDatabase } from './salesforce_catalog_update.js';
35 :
36 1 : const LOG_CTX = "salesforce_setup";
37 :
38 : /// The connectors that share the Salesforce setup
39 : export type SalesforceConnectorType = ConnectorType.SALESFORCE_DATA_CLOUD | ConnectorType.SIDB;
40 :
41 : // We use the web-server OAuth Flow with or without consumer secret.
42 : //
43 : // !! Don't embed an External Client App secret !!
44 : //
45 : // For untrusted clients, like this SPA, the web server OAuth flow can be configure to NOT require a consumer secret but
46 : // still use PKCE. PKCE makes this more preferrable than the alternative user-agent flow for untrusted clients since it
47 : // ensures that the application that starts the authentication flow is the same one that finishes it.
48 : // (Salesforce discourages using the user-agent flow, see https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_user_agent_flow.htm&type=5)
49 : //
50 : // Make sure this is checked (should be by default):
51 : // Setup > App Manager > Your App > "Require Proof Key for Code Exchange (PKCE)"
52 : // Uncheck this:
53 : // Setup > App Manager > Your App > "Require Secret for Web Server Flow"
54 : // What you'll eventually need as well (not, if you only use the native apps):
55 : // Setup > CORS > Enable CORS for OAuth endpoints
56 : // Setup > CORS > Allowed Origins List > Add your Origin
57 : //
58 : // Docs:
59 : // - Web Server Flow: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm&type=5
60 : // - User Agent Flow: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_user_agent_flow.htm&type=5
61 : // - PKCE: https://oauth.net/2/pkce/
62 : //
63 : // PKCE flow:
64 : // 1. Client creates the code_verifier. (RFC 7636, Section 4.1)
65 : // 2. Client creates the code_challenge by transforming the code_verifier using S256 encryption. (RFC 7636, Section 4.2)
66 : // 3. Client sends the code_challenge and code_challenge_method with the initial authorization request. (RFC 7636, Section 4.3)
67 : // 4. Server responds with an authorization_code. (RFC 7636, Section 4.4)
68 : // 5. Client sends authorization_code and code_verifier to the token endpoint. (RFC 7636, Section 4.5)
69 : // 6. Server transforms the code_verifier using the code_challenge_method from the initial authorization request and checks the result against the code_challenge. If the value of both strings match, then the server has verified that the requests came from the same client and will issue an access_token. (RFC 7636, Section 4.6)
70 :
71 1 : export async function setupSalesforceConnection(modifyState: Dispatch<SalesforceConnectionStateAction>, logger: Logger, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, config: SalesforceConnectorConfig, platformType: PlatformType, apiClient: SalesforceApiClientInterface, httpClient: HyperDatabaseClient | null, appEvents: PlatformEventListener, forceReLogin: boolean, abortSignal: AbortSignal): Promise<SalesforceDatabaseChannel> {
72 1 : params = structuredClone(params);
73 1 : let hyperChannel: HyperDatabaseChannel;
74 1 : let sfChannel: SalesforceDatabaseChannel;
75 1 : try {
76 : // Start the authorization process
77 1 : modifyState({
78 1 : type: SETUP_STARTED,
79 1 : value: params,
80 1 : });
81 1 : abortSignal.throwIfAborted();
82 1 : if (!config.auth?.oauthRedirect) {
83 1 : throw new Error(`missing oauth redirect url`);
84 : }
85 1 : const onAuthenticationProgress = (progress: SalesforceAuthenticationProgress) => {
86 1 : switch (progress.stage) {
87 1 : case 'GENERATING_PKCE_CHALLENGE':
88 1 : modifyState({ type: GENERATING_PKCE_CHALLENGE, value: null });
89 1 : break;
90 1 : case 'GENERATED_PKCE_CHALLENGE':
91 1 : modifyState({ type: GENERATED_PKCE_CHALLENGE, value: progress.pkceChallenge });
92 1 : break;
93 0 : case 'OAUTH_NATIVE_LINK_OPENED':
94 0 : modifyState({ type: OAUTH_NATIVE_LINK_OPENED, value: null });
95 0 : break;
96 1 : case 'OAUTH_WEB_WINDOW_OPENED':
97 1 : modifyState({ type: OAUTH_WEB_WINDOW_OPENED, value: null });
98 1 : break;
99 1 : case 'OAUTH_WEB_WINDOW_CLOSED':
100 1 : modifyState({ type: OAUTH_WEB_WINDOW_CLOSED, value: null });
101 1 : break;
102 1 : case 'RECEIVED_CORE_AUTH_CODE':
103 1 : modifyState({
104 1 : type: RECEIVED_CORE_AUTH_CODE,
105 1 : value: { token: progress.code, createdAt: dateToTimestamp(new Date())! },
106 1 : });
107 1 : break;
108 1 : case 'REQUESTING_CORE_AUTH_TOKEN':
109 1 : modifyState({ type: REQUESTING_CORE_AUTH_TOKEN, value: null });
110 1 : break;
111 1 : case 'RECEIVED_CORE_AUTH_TOKEN':
112 1 : modifyState({ type: RECEIVED_CORE_AUTH_TOKEN, value: progress.coreAccessToken });
113 1 : break;
114 1 : case 'RECEIVED_CORE_USER_INFO':
115 1 : modifyState({ type: RECEIVED_CORE_USER_INFO, value: progress.coreUserInfo });
116 1 : break;
117 1 : case 'REQUESTING_DATA_CLOUD_ACCESS_TOKEN':
118 1 : modifyState({ type: REQUESTING_DATA_CLOUD_ACCESS_TOKEN, value: null });
119 1 : break;
120 1 : case 'RECEIVED_DATA_CLOUD_ACCESS_TOKEN':
121 1 : modifyState({ type: RECEIVED_DATA_CLOUD_ACCESS_TOKEN, value: progress.dataCloudAccessToken });
122 : break;
123 1 : }
124 1 : };
125 1 : const { coreAccessToken, dataCloudAccessToken: dcToken } = await authenticateSalesforce({
126 1 : logger,
127 1 : params,
128 1 : authConfig: config.auth,
129 1 : platformType,
130 1 : apiClient,
131 1 : appEvents,
132 1 : forceReLogin,
133 1 : abortSignal,
134 1 : onProgress: onAuthenticationProgress,
135 1 : });
136 1 : abortSignal.throwIfAborted();
137 :
138 : // Start the channel setup, Salesforce connections speak to Hyper over http
139 1 : const connParams: connection.HyperConnectionParams = {
140 1 : protocol: 'V3_HTTP',
141 1 : endpoint: dcToken.instanceUrl ?? "",
142 1 : tls: {
143 1 : clientKeyPath: "",
144 1 : clientCertPath: "",
145 1 : caCertsPath: ""
146 1 : },
147 1 : attachedDatabases: [],
148 1 : metadata: {
149 1 : message: "",
150 1 : details: {}
151 1 : } as any,
152 1 : };
153 1 : modifyState({
154 1 : type: SF_CHANNEL_SETUP_STARTED,
155 1 : value: connParams,
156 1 : });
157 1 : abortSignal.throwIfAborted()
158 :
159 : // Static connection context.
160 : // Inject the database name, the audience header and the bearer token
161 1 : const authInfo = collectSalesforceAuthInfo(coreAccessToken, dcToken);
162 : // SIDB queries the configured Hyper Cloud database
163 1 : const attachedDatabases: AttachedDatabase[] = [];
164 1 : if (connector === ConnectorType.SIDB) {
165 1 : attachedDatabases.push(getSidbAttachedDatabase(params));
166 : }
167 1 : const connectionContext: HyperDatabaseConnectionContext = {
168 1 : getAttachedDatabases(): AttachedDatabase[] {
169 1 : return attachedDatabases;
170 1 : },
171 1 : async getRequestMetadata(): Promise<Record<string, string>> {
172 1 : return {
173 1 : audience: authInfo?.offcoreTenantId ?? "",
174 1 : authorization: `Bearer ${authInfo?.offcoreRawJwt}`,
175 1 : };
176 1 : },
177 0 : getQueryParameters(): Record<string, string> {
178 0 : return {};
179 0 : }
180 1 : };
181 :
182 : // Create the channel
183 1 : if (!httpClient) throw new Error(`No client available for protocol ${connParams.protocol}`);
184 1 : hyperChannel = await httpClient.connect(connParams, connectionContext);
185 1 : sfChannel = new SalesforceDatabaseChannel(apiClient, coreAccessToken, dcToken, hyperChannel);
186 1 : abortSignal.throwIfAborted();
187 :
188 : // Mark the channel as ready
189 1 : modifyState({
190 1 : type: SF_CHANNEL_READY,
191 1 : value: sfChannel,
192 1 : });
193 1 : abortSignal.throwIfAborted();
194 :
195 1 : } catch (error: any) {
196 1 : if (error.name === 'AbortError') {
197 1 : logger.info("Cancelled OAuth flow", {}, LOG_CTX);
198 1 : modifyState({
199 1 : type: SETUP_CANCELLED,
200 1 : value: error,
201 1 : });
202 1 : } else if (error instanceof Error) {
203 1 : logger.warn("Failed OAuth flow", { "error": stringifyError(error) }, LOG_CTX);
204 1 : modifyState({
205 1 : type: SETUP_FAILED,
206 1 : value: {
207 1 : message: error.message,
208 1 : },
209 1 : });
210 1 : }
211 : // Rethrow the error
212 1 : throw error;
213 1 : }
214 :
215 1 : return sfChannel;
216 1 : }
217 :
218 : export interface SalesforceSetupApi {
219 : setup(dispatch: Dispatch<SalesforceConnectionStateAction>, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, abortSignal: AbortSignal): Promise<SalesforceDatabaseChannel>
220 : reset(dispatch: Dispatch<SalesforceConnectionStateAction>): Promise<void>
221 : }
222 :
223 1 : export function createSalesforceSetup(httpClient: HyperDatabaseClient | null, salesforceApi: SalesforceApiClientInterface, platformType: PlatformType, appEvents: PlatformEventListener, config: SalesforceConnectorConfig, forceReLogin: boolean, logger: Logger): (SalesforceSetupApi | null) {
224 1 : const setup = async (updateState: Dispatch<SalesforceConnectionStateAction>, connector: SalesforceConnectorType, params: connection.SalesforceConnectionParams, abort: AbortSignal) => {
225 1 : return setupSalesforceConnection(updateState, logger, connector, params, config, platformType, salesforceApi, httpClient, appEvents, forceReLogin, abort);
226 1 : };
227 1 : const reset = async (updateState: Dispatch<SalesforceConnectionStateAction>) => {
228 1 : updateState({
229 1 : type: RESET_CONNECTION,
230 1 : value: null,
231 1 : });
232 1 : };
233 1 : return { setup, reset };
234 1 : };
|