Abstract OAuth signing and make credentials extendable

This commit is contained in:
Jan Oberhauser
2020-01-13 20:46:58 -06:00
parent 740cb8a6fc
commit 8228b8505f
13 changed files with 283 additions and 15 deletions

View File

@@ -39,6 +39,7 @@
"typescript": "~3.7.4"
},
"dependencies": {
"client-oauth2": "^4.2.5",
"cron": "^1.7.2",
"crypto-js": "^3.1.9-1",
"lodash.get": "^4.4.2",

View File

@@ -1,4 +1,5 @@
import {
IAllExecuteFunctions,
IBinaryData,
ICredentialType,
IDataObject,
@@ -18,6 +19,7 @@ import {
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import * as requestPromise from 'request-promise-native';
interface Constructable<T> {
@@ -35,6 +37,7 @@ export interface IExecuteFunctions extends IExecuteFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@@ -44,6 +47,7 @@ export interface IExecuteSingleFunctions extends IExecuteSingleFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@@ -52,15 +56,22 @@ export interface IPollFunctions extends IPollFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
export interface IResponseError extends Error {
statusCode?: number;
}
export interface ITriggerFunctions extends ITriggerFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@@ -84,6 +95,7 @@ export interface IUserSettings {
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
helpers: {
request?: requestPromise.RequestPromiseAPI,
requestOAuth?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise<any>, // tslint:disable-line:no-any
};
}
@@ -91,6 +103,7 @@ export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
export interface IHookFunctions extends IHookFunctionsBase {
helpers: {
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@@ -99,6 +112,7 @@ export interface IWebhookFunctions extends IWebhookFunctionsBase {
helpers: {
prepareBinaryData(binaryData: Buffer, filePath?: string, mimeType?: string): Promise<IBinaryData>;
request: requestPromise.RequestPromiseAPI,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}

View File

@@ -2,11 +2,13 @@ import {
Credentials,
IHookFunctions,
ILoadOptionsFunctions,
IResponseError,
IWorkflowSettings,
BINARY_ENCODING,
} from './';
import {
IAllExecuteFunctions,
IBinaryData,
IContextObject,
ICredentialDataDecryptedObject,
@@ -34,9 +36,11 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import { get } from 'lodash';
import * as clientOAuth2 from 'client-oauth2';
import { get, unset } from 'lodash';
import * as express from "express";
import * as path from 'path';
import { OptionsWithUri } from 'request';
import * as requestPromise from 'request-promise-native';
import { Magic, MAGIC_MIME_TYPE } from 'mmmagic';
@@ -101,6 +105,70 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m
/**
* Makes a request using OAuth data for authentication
*
* @export
* @param {IAllExecuteFunctions} this
* @param {string} credentialsType
* @param {(OptionsWithUri | requestPromise.RequestPromiseOptions)} requestOptions
* @param {INode} node
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns
*/
export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData) {
const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject;
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
if (credentials.oauthTokenData === undefined) {
throw new Error('OAuth credentials not connected!');
}
const oAuthClient = new clientOAuth2({});
const oauthTokenData = JSON.parse(credentials.oauthTokenData as string);
const token = oAuthClient.createToken(oauthTokenData);
// Signs the request by adding authorization headers or query parameters depending
// on the token-type used.
const newRequestOptions = token.sign(requestOptions as clientOAuth2.RequestObject);
return this.helpers.request!(newRequestOptions)
.catch(async (error: IResponseError) => {
// TODO: Check if also other codes are possible
if (error.statusCode === 401) {
// TODO: Whole refresh process is not tested yet
// Token is probably not valid anymore. So try refresh it.
const newToken = await token.refresh();
const newCredentialsData = newToken.data;
unset(newCredentialsData, 'csrfSecret');
unset(newCredentialsData, 'oauthTokenData');
// Find the name of the credentials
if (!node.credentials || !node.credentials[credentialsType]) {
throw new Error(`The node "${node.name}" does not have credentials of type "${credentialsType}"!`);
}
const name = node.credentials[credentialsType];
// Save the refreshed token
await additionalData.updateCredentials(name, credentialsType, newCredentialsData, additionalData.encryptionKey);
// Make the request again with the new token
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
return this.helpers.request!(newRequestOptions);
}
// Unknown error so simply throw it
throw error;
});
}
/**
* Takes generic input data and brings it into the json format n8n uses.
*
@@ -355,6 +423,9 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
returnJsonArray,
},
};
@@ -406,6 +477,9 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
returnJsonArray,
},
};
@@ -484,6 +558,9 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
returnJsonArray,
},
};
@@ -563,6 +640,9 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
@@ -610,6 +690,9 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
},
};
return that;
@@ -665,6 +748,9 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
},
};
return that;
@@ -747,6 +833,9 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any> {
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData);
},
returnJsonArray,
},
};

View File

@@ -1,6 +1,7 @@
import { set } from 'lodash';
import {
ICredentialDataDecryptedObject,
IExecuteWorkflowInfo,
INodeExecutionData,
INodeParameters,
@@ -280,6 +281,7 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise<IRun
restApiUrl: '',
encryptionKey: 'test',
timezone: 'America/New_York',
updateCredentials: async (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise<void> => {},
webhookBaseUrl: 'webhook',
webhookTestBaseUrl: 'webhook-test',
};