Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ› Handle Wise SCA requests #2734

Merged
merged 1 commit into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
axiosRequest.headers['User-Agent'] = 'n8n';
}

if (n8nRequest.ignoreHttpStatusErrors) {
axiosRequest.validateStatus = () => true;
}

return axiosRequest;
}

Expand Down
10 changes: 10 additions & 0 deletions packages/nodes-base/credentials/WiseApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
];
}
71 changes: 61 additions & 10 deletions packages/nodes-base/nodes/Wise/GenericFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
import {
createSign,
} from 'crypto';

import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';

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) {
Expand All @@ -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;
ivov marked this conversation as resolved.
Show resolved Hide resolved
}

// 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},);
}
}

/**
Expand Down Expand Up @@ -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 = {
Expand Down
26 changes: 20 additions & 6 deletions packages/nodes-base/nodes/Wise/Wise.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import {
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
Expand Down Expand Up @@ -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<INodePropertyOptions[]>((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;
}, []);
},
},
};
Expand Down
1 change: 1 addition & 0 deletions packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export interface IHttpRequestOptions {
encoding?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream';
skipSslCertificateValidation?: boolean;
returnFullResponse?: boolean;
ignoreHttpStatusErrors?: boolean;
proxy?: {
host: string;
port: number;
Expand Down