mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57: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';
|
axiosRequest.headers['User-Agent'] = 'n8n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (n8nRequest.ignoreHttpStatusErrors) {
|
||||||
|
axiosRequest.validateStatus = () => true;
|
||||||
|
}
|
||||||
|
|
||||||
return axiosRequest;
|
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 {
|
import {
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
IHookFunctions,
|
IHookFunctions,
|
||||||
|
@ -5,45 +9,45 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
IHttpRequestOptions,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
NodeApiError,
|
NodeApiError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
|
||||||
OptionsWithUri,
|
|
||||||
} from 'request';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make an authenticated API request to Wise.
|
* Make an authenticated API request to Wise.
|
||||||
*/
|
*/
|
||||||
export async function wiseApiRequest(
|
export async function wiseApiRequest(
|
||||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||||
method: string,
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH',
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
body: IDataObject = {},
|
body: IDataObject = {},
|
||||||
qs: IDataObject = {},
|
qs: IDataObject = {},
|
||||||
option: IDataObject = {},
|
option: IDataObject = {},
|
||||||
) {
|
) {
|
||||||
const { apiToken, environment } = await this.getCredentials('wiseApi') as {
|
const { apiToken, environment, privateKey } = await this.getCredentials('wiseApi') as {
|
||||||
apiToken: string,
|
apiToken: string,
|
||||||
environment: 'live' | 'test',
|
environment: 'live' | 'test',
|
||||||
|
privateKey?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootUrl = environment === 'live'
|
const rootUrl = environment === 'live'
|
||||||
? 'https://api.transferwise.com/'
|
? 'https://api.transferwise.com/'
|
||||||
: 'https://api.sandbox.transferwise.tech/';
|
: 'https://api.sandbox.transferwise.tech/';
|
||||||
|
|
||||||
const options: OptionsWithUri = {
|
const options: IHttpRequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'user-agent': 'n8n',
|
'user-agent': 'n8n',
|
||||||
'Authorization': `Bearer ${apiToken}`,
|
'Authorization': `Bearer ${apiToken}`,
|
||||||
},
|
},
|
||||||
method,
|
method,
|
||||||
uri: `${rootUrl}${endpoint}`,
|
url: `${rootUrl}${endpoint}`,
|
||||||
qs,
|
qs,
|
||||||
body,
|
body,
|
||||||
json: true,
|
json: true,
|
||||||
|
returnFullResponse: true,
|
||||||
|
ignoreHttpStatusErrors: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!Object.keys(body).length) {
|
if (!Object.keys(body).length) {
|
||||||
|
@ -58,11 +62,54 @@ export async function wiseApiRequest(
|
||||||
Object.assign(options, option);
|
Object.assign(options, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
try {
|
try {
|
||||||
return await this.helpers.request!(options);
|
response = await this.helpers.httpRequest!(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
delete error.config;
|
||||||
throw new NodeApiError(this.getNode(), error);
|
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 = {
|
export type Recipient = {
|
||||||
|
active: boolean,
|
||||||
id: number,
|
id: number,
|
||||||
accountHolderName: string
|
accountHolderName: string,
|
||||||
|
country: string | null,
|
||||||
|
currency: string,
|
||||||
|
type: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StatementAdditionalFields = {
|
export type StatementAdditionalFields = {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
import {
|
import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
|
INodePropertyOptions,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -141,12 +142,25 @@ export class Wise implements INodeType {
|
||||||
profileId: this.getNodeParameter('profileId', 0),
|
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) => ({
|
return recipients.reduce<INodePropertyOptions[]>((activeRecipients, {
|
||||||
name: accountHolderName,
|
active,
|
||||||
value: id,
|
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';
|
encoding?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream';
|
||||||
skipSslCertificateValidation?: boolean;
|
skipSslCertificateValidation?: boolean;
|
||||||
returnFullResponse?: boolean;
|
returnFullResponse?: boolean;
|
||||||
|
ignoreHttpStatusErrors?: boolean;
|
||||||
proxy?: {
|
proxy?: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
|
Loading…
Reference in a new issue