From f350b9e1c07c9ac179586ee2184efce7e549dcd7 Mon Sep 17 00:00:00 2001 From: pemontto <939704+pemontto@users.noreply.github.com> Date: Sun, 6 Mar 2022 10:41:01 +0000 Subject: [PATCH] :bug: Handle Wise SCA requests (#2734) --- packages/core/src/NodeExecuteFunctions.ts | 4 ++ .../credentials/WiseApi.credentials.ts | 10 +++ .../nodes-base/nodes/Wise/GenericFunctions.ts | 71 ++++++++++++++++--- packages/nodes-base/nodes/Wise/Wise.node.ts | 26 +++++-- packages/workflow/src/Interfaces.ts | 1 + 5 files changed, 96 insertions(+), 16 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 3116b7e244784..421314e61ae89 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -744,6 +744,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest axiosRequest.headers['User-Agent'] = 'n8n'; } + if (n8nRequest.ignoreHttpStatusErrors) { + axiosRequest.validateStatus = () => true; + } + return axiosRequest; } diff --git a/packages/nodes-base/credentials/WiseApi.credentials.ts b/packages/nodes-base/credentials/WiseApi.credentials.ts index f3d558771bdd1..ce0a2723b024f 100644 --- a/packages/nodes-base/credentials/WiseApi.credentials.ts +++ b/packages/nodes-base/credentials/WiseApi.credentials.ts @@ -30,5 +30,15 @@ export class WiseApi implements ICredentialType { }, ], }, + { + displayName: 'Private Key (Optional)', + name: 'privateKey', + type: 'string', + default: '', + description: 'Optional private key used for Strong Customer Authentication (SCA). Only needed to retrieve statements, and execute transfers.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, ]; } diff --git a/packages/nodes-base/nodes/Wise/GenericFunctions.ts b/packages/nodes-base/nodes/Wise/GenericFunctions.ts index 785a2097b43f5..9395dd80b0ece 100644 --- a/packages/nodes-base/nodes/Wise/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Wise/GenericFunctions.ts @@ -1,3 +1,7 @@ +import { + createSign, +} from 'crypto'; + import { IExecuteFunctions, IHookFunctions, @@ -5,45 +9,45 @@ import { import { IDataObject, + IHttpRequestOptions, ILoadOptionsFunctions, INodeExecutionData, NodeApiError, } from 'n8n-workflow'; -import { - OptionsWithUri, -} from 'request'; - /** * Make an authenticated API request to Wise. */ export async function wiseApiRequest( this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, - method: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH', endpoint: string, body: IDataObject = {}, qs: IDataObject = {}, option: IDataObject = {}, ) { - const { apiToken, environment } = await this.getCredentials('wiseApi') as { + const { apiToken, environment, privateKey } = await this.getCredentials('wiseApi') as { apiToken: string, environment: 'live' | 'test', + privateKey?: string, }; const rootUrl = environment === 'live' ? 'https://api.transferwise.com/' : 'https://api.sandbox.transferwise.tech/'; - const options: OptionsWithUri = { + const options: IHttpRequestOptions = { headers: { 'user-agent': 'n8n', 'Authorization': `Bearer ${apiToken}`, }, method, - uri: `${rootUrl}${endpoint}`, + url: `${rootUrl}${endpoint}`, qs, body, json: true, + returnFullResponse: true, + ignoreHttpStatusErrors: true, }; if (!Object.keys(body).length) { @@ -58,11 +62,54 @@ export async function wiseApiRequest( Object.assign(options, option); } + let response; try { - return await this.helpers.request!(options); + response = await this.helpers.httpRequest!(options); } catch (error) { + delete error.config; throw new NodeApiError(this.getNode(), error); } + + if (response.statusCode === 200) { + return response.body; + } + + // Request requires SCA approval + if (response.statusCode === 403 && response.headers['x-2fa-approval']) { + if (!privateKey) { + throw new NodeApiError(this.getNode(), { + message: 'This request requires Strong Customer Authentication (SCA). Please add a key pair to your account and n8n credentials. See https://api-docs.transferwise.com/#strong-customer-authentication-personal-token', + headers: response.headers, + body: response.body, + }); + } + // Sign the x-2fa-approval + const oneTimeToken = response.headers['x-2fa-approval'] as string; + const signerObject = createSign('RSA-SHA256').update(oneTimeToken); + try { + const signature = signerObject.sign( + privateKey, + 'base64', + ); + delete option.ignoreHttpStatusErrors; + options.headers = { + ...options.headers, + 'X-Signature': signature, + 'x-2fa-approval': oneTimeToken, + }; + } catch (error) { + throw new NodeApiError(this.getNode(), {message: 'Error signing SCA request, check your private key', ...error}); + } + // Retry the request with signed token + try { + response = await this.helpers.httpRequest!(options); + return response.body; + } catch (error) { + throw new NodeApiError(this.getNode(), {message: 'SCA request failed, check your private key is valid'}); + } + } else { + throw new NodeApiError(this.getNode(), {headers: response.headers, body: response.body},); + } } /** @@ -113,8 +160,12 @@ export type Profile = { }; export type Recipient = { + active: boolean, id: number, - accountHolderName: string + accountHolderName: string, + country: string | null, + currency: string, + type: string, }; export type StatementAdditionalFields = { diff --git a/packages/nodes-base/nodes/Wise/Wise.node.ts b/packages/nodes-base/nodes/Wise/Wise.node.ts index 34917427357ce..1063a33821934 100644 --- a/packages/nodes-base/nodes/Wise/Wise.node.ts +++ b/packages/nodes-base/nodes/Wise/Wise.node.ts @@ -5,6 +5,7 @@ import { import { IDataObject, ILoadOptionsFunctions, + INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; @@ -141,12 +142,25 @@ export class Wise implements INodeType { profileId: this.getNodeParameter('profileId', 0), }; - const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs); - - return recipients.map(({ id, accountHolderName }: Recipient) => ({ - name: accountHolderName, - value: id, - })); + const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs) as Recipient[]; + + return recipients.reduce((activeRecipients, { + active, + id, + accountHolderName, + currency, + country, + type, + }) => { + if (active) { + const recipient = { + name: `[${currency}] ${accountHolderName} - (${country !== null ? country + ' - ' : '' }${type})`, + value: id, + }; + activeRecipients.push(recipient); + } + return activeRecipients; + }, []); }, }, }; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 8a5b04a4bd284..09ffaac476d49 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -420,6 +420,7 @@ export interface IHttpRequestOptions { encoding?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'; skipSslCertificateValidation?: boolean; returnFullResponse?: boolean; + ignoreHttpStatusErrors?: boolean; proxy?: { host: string; port: number;