mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
🐛 Handle Wise SCA requests (#2734)
This commit is contained in:
parent
fbdb5eb0fa
commit
f350b9e1c0
|
@ -744,6 +744,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
|
|||
axiosRequest.headers['User-Agent'] = 'n8n';
|
||||
}
|
||||
|
||||
if (n8nRequest.ignoreHttpStatusErrors) {
|
||||
axiosRequest.validateStatus = () => true;
|
||||
}
|
||||
|
||||
return axiosRequest;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs) as Recipient[];
|
||||
|
||||
return recipients.map(({ id, accountHolderName }: Recipient) => ({
|
||||
name: accountHolderName,
|
||||
value: id,
|
||||
}));
|
||||
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;
|
||||
}, []);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue