🔀 Merge branch 'RicardoE105-feature/oauth1-support' into oauth-support

This commit is contained in:
Jan Oberhauser 2020-06-03 15:07:00 +02:00
commit aff24bb1a5
27 changed files with 777 additions and 45 deletions

View file

@ -103,6 +103,7 @@
"n8n-editor-ui": "~0.45.0",
"n8n-nodes-base": "~0.62.1",
"n8n-workflow": "~0.31.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View file

@ -13,10 +13,13 @@ import {
import * as bodyParser from 'body-parser';
require('body-parser-xml')(bodyParser);
import * as history from 'connect-history-api-fallback';
import * as requestPromise from 'request-promise-native';
import * as _ from 'lodash';
import * as clientOAuth2 from 'client-oauth2';
import * as clientOAuth1 from 'oauth-1.0a';
import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto';
import {
ActiveExecutions,
@ -90,7 +93,8 @@ import * as jwks from 'jwks-rsa';
// @ts-ignore
import * as timezones from 'google-timezones-json';
import * as parseUrl from 'parseurl';
import * as querystring from 'querystring';
import { OptionsWithUrl } from 'request-promise-native';
class App {
@ -890,6 +894,158 @@ class App {
return returnData;
}));
// ----------------------------------------
// OAuth1-Credential/Auth
// ----------------------------------------
// Authorize OAuth Data
this.app.get('/rest/oauth1-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
if (req.query.id === undefined) {
throw new Error('Required credential id is missing!');
}
const result = await Db.collections.Credentials!.findOne(req.query.id as string);
if (result === undefined) {
res.status(404).send('The credential is not known.');
return '';
}
let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) {
throw new Error('No encryption key got found to decrypt the credentials!');
}
// Decrypt the currently saved credentials
const workflowCredentials: IWorkflowCredentials = {
[result.type as string]: {
[result.name as string]: result as ICredentialsEncrypted,
},
};
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
const signatureMethod = _.get(oauthCredentials, 'signatureMethod') as string;
const oauth = new clientOAuth1({
consumer: {
key: _.get(oauthCredentials, 'consumerKey') as string,
secret: _.get(oauthCredentials, 'consumerSecret') as string,
},
signature_method: signatureMethod,
hash_function(base, key) {
const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256';
return createHmac(algorithm, key)
.update(base)
.digest('base64');
},
});
const callback = `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth1-credential/callback?cid=${req.query.id}`;
const options: RequestOptions = {
method: 'POST',
url: (_.get(oauthCredentials, 'requestTokenUrl') as string),
data: {
oauth_callback: callback,
},
};
const data = oauth.toHeader(oauth.authorize(options as RequestOptions));
//@ts-ignore
options.headers = data;
const response = await requestPromise(options);
// Response comes as x-www-form-urlencoded string so convert it to JSON
const responseJson = querystring.parse(response);
const returnUri = `${_.get(oauthCredentials, 'authUrl')}?oauth_token=${responseJson.oauth_token}`;
// Encrypt the data
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
credentials.setData(decryptedDataOriginal, encryptionKey);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data
newCredentialsData.updatedAt = this.getCurrentDate();
// Update the credentials in DB
await Db.collections.Credentials!.update(req.query.id as string, newCredentialsData);
return returnUri;
}));
// Verify and store app code. Generate access tokens and store for respective credential.
this.app.get('/rest/oauth1-credential/callback', async (req: express.Request, res: express.Response) => {
const { oauth_verifier, oauth_token, cid } = req.query;
if (oauth_verifier === undefined || oauth_token === undefined) {
throw new Error('Insufficient parameters for OAuth1 callback');
}
const result = await Db.collections.Credentials!.findOne(cid as any); // tslint:disable-line:no-any
if (result === undefined) {
const errorResponse = new ResponseHelper.ResponseError('The credential is not known.', undefined, 404);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
let encryptionKey = undefined;
encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) {
const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
// Decrypt the currently saved credentials
const workflowCredentials: IWorkflowCredentials = {
[result.type as string]: {
[result.name as string]: result as ICredentialsEncrypted,
},
};
const credentialsHelper = new CredentialsHelper(workflowCredentials, encryptionKey);
const decryptedDataOriginal = credentialsHelper.getDecrypted(result.name, result.type, true);
const oauthCredentials = credentialsHelper.applyDefaultsAndOverwrites(decryptedDataOriginal, result.type);
const options: OptionsWithUrl = {
method: 'POST',
url: _.get(oauthCredentials, 'accessTokenUrl') as string,
qs: {
oauth_token,
oauth_verifier,
}
};
let oauthToken;
try {
oauthToken = await requestPromise(options);
} catch (error) {
const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
// Response comes as x-www-form-urlencoded string so convert it to JSON
const oauthTokenJson = querystring.parse(oauthToken);
decryptedDataOriginal.oauthTokenData = oauthTokenJson;
const credentials = new Credentials(result.name, result.type, result.nodesAccess);
credentials.setData(decryptedDataOriginal, encryptionKey);
const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb;
// Add special database related data
newCredentialsData.updatedAt = this.getCurrentDate();
// Save the credentials in DB
await Db.collections.Credentials!.update(cid as any, newCredentialsData); // tslint:disable-line:no-any
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
});
// ----------------------------------------
// OAuth2-Credential/Auth

View file

@ -18,7 +18,7 @@ import {
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
import { OptionsWithUri, OptionsWithUrl } from 'request';
import * as requestPromise from 'request-promise-native';
interface Constructable<T> {
@ -36,7 +36,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@ -46,7 +47,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -55,7 +57,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@ -70,7 +73,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}
@ -94,7 +98,8 @@ 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
requestOAuth2?: (this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions) => Promise<any>, // tslint:disable-line:no-any
requestOAuth1?(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -102,7 +107,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
};
}
@ -111,7 +117,8 @@ 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
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any>, // tslint:disable-line:no-any
returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExecutionData[];
};
}

View file

@ -36,15 +36,20 @@ import {
WorkflowExecuteMode,
} from 'n8n-workflow';
import * as clientOAuth1 from 'oauth-1.0a';
import { RequestOptions, Token } from 'oauth-1.0a';
import * as clientOAuth2 from 'client-oauth2';
import { get } from 'lodash';
import * as express from 'express';
import * as path from 'path';
import { OptionsWithUri } from 'request';
import { OptionsWithUrl, OptionsWithUri } from 'request';
import * as requestPromise from 'request-promise-native';
import { Magic, MAGIC_MIME_TYPE } from 'mmmagic';
import { createHmac } from 'crypto';
const magic = new Magic(MAGIC_MIME_TYPE);
@ -116,7 +121,7 @@ export async function prepareBinaryData(binaryData: Buffer, filePath?: string, m
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns
*/
export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) {
export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, node: INode, additionalData: IWorkflowExecuteAdditionalData, tokenType?: string, property?: string) {
const credentials = this.getCredentials(credentialsType) as ICredentialDataDecryptedObject;
if (credentials === undefined) {
@ -170,6 +175,63 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string
});
}
/* Makes a request using OAuth1 data for authentication
*
* @export
* @param {IAllExecuteFunctions} this
* @param {string} credentialsType
* @param {(OptionsWithUrl | requestPromise.RequestPromiseOptions)} requestOptionså
* @returns
*/
export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions) {
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 oauth = new clientOAuth1({
consumer: {
key: credentials.consumerKey as string,
secret: credentials.consumerSecret as string,
},
signature_method: credentials.signatureMethod as string,
hash_function(base, key) {
const algorithm = (credentials.signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256';
return createHmac(algorithm, key)
.update(base)
.digest('base64');
},
});
const oauthTokenData = credentials.oauthTokenData as IDataObject;
const token: Token = {
key: oauthTokenData.oauth_token as string,
secret: oauthTokenData.oauth_token_secret as string,
};
const newRequestOptions = {
//@ts-ignore
url: requestOptions.url,
method: requestOptions.method,
data: requestOptions.body,
json: requestOptions.json,
};
//@ts-ignore
newRequestOptions.form = oauth.authorize(newRequestOptions as RequestOptions, token);
return this.helpers.request!(newRequestOptions)
.catch(async (error: IResponseError) => {
// Unknown error so simply throw it
throw error;
});
}
/**
@ -462,8 +524,11 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
@ -522,8 +587,11 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
@ -615,8 +683,11 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},
@ -710,8 +781,11 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
@ -763,8 +837,11 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
@ -827,8 +904,11 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio
},
helpers: {
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
},
};
@ -918,8 +998,11 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi
helpers: {
prepareBinaryData,
request: requestPromise,
requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
requestOAuth2(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions, tokenType?: string, property?: string): Promise<any> { // tslint:disable-line:no-any
return requestOAuth2.call(this, credentialsType, requestOptions, node, additionalData, tokenType, property);
},
requestOAuth1(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUrl | requestPromise.RequestPromiseOptions): Promise<any> { // tslint:disable-line:no-any
return requestOAuth1.call(this, credentialsType, requestOptions);
},
returnJsonArray,
},

View file

@ -145,6 +145,7 @@ export interface IRestApi {
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
getTimezones(): Promise<IDataObject>;
oAuth1CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise<string>;
oAuth2Callback(code: string, state: string): Promise<string>;
}

View file

@ -47,13 +47,13 @@
Not all required credential properties are filled
</span>
<span v-else-if="isOAuthConnected === true">
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuth2CredentialAuthorize()" circle>
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="redo" />
</el-button>
Is connected
</span>
<span v-else>
<el-button title="Connect OAuth Credentials" @click.stop="oAuth2CredentialAuthorize()" circle>
<el-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="sign-in-alt" />
</el-button>
Is NOT connected
@ -219,11 +219,11 @@ export default mixins(
return this.credentialDataTemp;
},
isOAuthType (): boolean {
if (this.credentialTypeData.name === 'oAuth2Api') {
if (['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeData.name)) {
return true;
}
const types = this.parentTypes(this.credentialTypeData.name);
return types.includes('oAuth2Api');
return types.includes('oAuth1Api') || types.includes('oAuth2Api');
},
isOAuthConnected (): boolean {
if (this.isOAuthType === false) {
@ -233,7 +233,9 @@ export default mixins(
return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData;
},
oAuthCallbackUrl (): string {
return this.$store.getters.getWebhookBaseUrl + 'rest/oauth2-credential/callback';
const types = this.parentTypes(this.credentialTypeData.name);
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
return this.$store.getters.getWebhookBaseUrl + `rest/${oauthType}-credential/callback`;
},
requiredPropertiesFilled (): boolean {
for (const property of this.credentialProperties) {
@ -323,7 +325,7 @@ export default mixins(
return result;
},
async oAuth2CredentialAuthorize () {
async oAuthCredentialAuthorize () {
let url;
let credentialData = this.credentialDataDynamic;
@ -350,8 +352,14 @@ export default mixins(
}
}
const types = this.parentTypes(this.credentialTypeData.name);
try {
url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string;
if (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) {
url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string;
} else if (this.credentialTypeData.name === 'oAuth1Api' || types.includes('oAuth1Api')) {
url = await this.restApi().oAuth1CredentialAuthorize(credentialData as ICredentialsResponse) as string;
}
} catch (error) {
this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:');
return;

View file

@ -252,6 +252,11 @@ export const restApi = Vue.extend({
return self.restApi().makeRestApiRequest('GET', `/credential-types`);
},
// Get OAuth1 Authorization URL using the stored credentials
oAuth1CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
return self.restApi().makeRestApiRequest('GET', `/oauth1-credential/auth`, sendData);
},
// Get OAuth2 Authorization URL using the stored credentials
oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise<string> => {
return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData);

View file

@ -0,0 +1,63 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class OAuth1Api implements ICredentialType {
name = 'oAuth1Api';
displayName = 'OAuth1 API';
properties = [
{
displayName: 'Consumer Key',
name: 'consumerKey',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Consumer Secret',
name: 'consumerSecret',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Request Token URL',
name: 'requestTokenUrl',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
},
{
displayName: 'Signature Method',
name: 'signatureMethod',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'HMAC-SHA1',
value: 'HMAC-SHA1'
},
{
name: 'HMAC-SHA256',
value: 'HMAC-SHA256'
},
],
default: '',
required: true,
},
];
}

View file

@ -0,0 +1,38 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class TwitterOAuth1Api implements ICredentialType {
name = 'twitterOAuth1Api';
extends = [
'oAuth1Api',
];
displayName = 'Twitter OAuth API';
properties = [
{
displayName: 'Request Token URL',
name: 'requestTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.twitter.com/oauth/request_token',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.twitter.com/oauth/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.twitter.com/oauth/access_token',
},
{
displayName: 'Signature Method',
name: 'signatureMethod',
type: 'hidden' as NodePropertyTypes,
default: 'HMAC-SHA1',
},
];
}

View file

@ -51,7 +51,7 @@ export async function githubApiRequest(this: IHookFunctions | IExecuteFunctions,
const baseUrl = credentials!.server || 'https://api.github.com';
options.uri = `${baseUrl}${endpoint}`;
return await this.helpers.requestOAuth.call(this, 'githubOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'githubOAuth2Api', options);
}
} catch (error) {
if (error.statusCode === 401) {

View file

@ -31,7 +31,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'googleCalendarOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'googleCalendarOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier

View file

@ -50,7 +50,7 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF
return await this.helpers.request(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'googleSheetsOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'googleSheetsOAuth2Api', options);
}
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {

View file

@ -32,7 +32,7 @@ export async function helpscoutApiRequest(this: IExecuteFunctions | IExecuteSing
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'helpScoutOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'helpScoutOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body
&& error.response.body._embedded

View file

@ -801,7 +801,7 @@ export class HttpRequest implements INodeType {
// Now that the options are all set make the actual http request
if (oAuth2Api !== undefined) {
response = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions);
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
} else {
response = await this.helpers.request(requestOptions);
}

View file

@ -37,7 +37,7 @@ export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions |
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'keapOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'keapOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier

View file

@ -24,7 +24,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing
options.headers = Object.assign({}, options.headers, headers);
}
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'microsoftExcelOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'microsoftExcelOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) {
// Try to return the error prettier

View file

@ -35,7 +35,7 @@ export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSing
}
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'microsoftOneDriveOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'microsoftOneDriveOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) {
// Try to return the error prettier

View file

@ -138,7 +138,7 @@ export class OAuth implements INodeType {
json: true,
};
const responseData = await this.helpers.requestOAuth.call(this, 'oAuth2Api', requestOptions);
const responseData = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions);
return [this.helpers.returnJsonArray(responseData)];
} else {
throw new Error('Unknown operation');

View file

@ -20,7 +20,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin
};
try {
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'salesforceOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'salesforceOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body[0] && error.response.body[0].message) {
// Try to return the error prettier

View file

@ -43,7 +43,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu
return await this.helpers.request(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token');
return await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, 'bearer', 'authed_user.access_token');
}
} catch (error) {
if (error.statusCode === 401) {

View file

@ -0,0 +1,44 @@
import {
OptionsWithUrl,
} from 'request';
import {
IHookFunctions,
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function twitterApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
let options: OptionsWithUrl = {
method,
body,
qs,
url: uri || `https://api.twitter.com/1.1${resource}`,
json: true
};
try {
if (Object.keys(option).length !== 0) {
options = Object.assign({}, options, option);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth1.call(this, 'twitterOAuth1Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.errors) {
// Try to return the error prettier
const errorMessages = error.response.body.errors.map((error: IDataObject) => {
return error.message;
});
throw new Error(`Twitter error response [${error.statusCode}]: ${errorMessages.join(' | ')}`);
}
throw error;
}
}

View file

@ -0,0 +1,170 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const tweetOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'tweet',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new tweet',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const tweetFields = [
/* -------------------------------------------------------------------------- */
/* tweet:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Text',
name: 'text',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
required: true,
default: '',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'tweet',
],
},
},
description: 'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts. ',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'tweet',
],
},
},
options: [
{
displayName: 'Attachments',
name: 'attachmentsUi',
placeholder: 'Add Attachments',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachment',
displayName: 'Attachment',
values: [
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: '',
description: 'Name of the binary properties which contain data which should be added to tweet as attachment',
},
{
displayName: 'Category',
name: 'category',
type: 'options',
options: [
{
name: 'Amplify Video',
value: 'amplifyVideo',
},
{
name: 'Gif',
value: 'tweetGif',
},
{
name: 'Image',
value: 'tweetImage',
},
{
name: 'Video',
value: 'tweetVideo',
},
],
default: '',
description: 'The category that represents how the media will be used',
},
],
},
],
default: '',
description: 'Array of supported attachments to add to the message.',
},
{
displayName: 'Display Coordinates',
name: 'displayCoordinates',
type: 'boolean',
default: false,
description: 'Whether or not to put a pin on the exact coordinates a Tweet has been sent from.',
},
{
displayName: 'Location',
name: 'locationFieldsUi',
type: 'fixedCollection',
placeholder: 'Add Location',
default: {},
description: `Subscriber location information.n`,
options: [
{
name: 'locationFieldsValues',
displayName: 'Location',
values: [
{
displayName: 'Latitude',
name: 'latitude',
type: 'string',
required: true,
description: 'The location latitude.',
default: '',
},
{
displayName: 'Longitude',
name: 'longitude',
type: 'string',
required: true,
description: 'The location longitude.',
default: '',
},
],
},
],
},
{
displayName: 'Possibly Sensitive',
name: 'possiblySensitive',
type: 'boolean',
default: false,
description: 'If you upload Tweet media that might be considered sensitive content such as nudity, or medical procedures, you must set this value to true.',
},
]
},
] as INodeProperties[];

View file

@ -0,0 +1,8 @@
export interface ITweet {
display_coordinates?: boolean;
lat?: number;
long?: number;
media_ids?: string;
possibly_sensitive?: boolean;
status: string;
}

View file

@ -0,0 +1,145 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
tweetFields,
tweetOperations,
} from './TweetDescription';
import {
twitterApiRequest,
} from './GenericFunctions';
import {
ITweet,
} from './TweetInterface';
import {
snakeCase,
} from 'change-case';
export class Twitter implements INodeType {
description: INodeTypeDescription = {
displayName: 'Twitter ',
name: 'twitter',
icon: 'file:twitter.png',
group: ['input', 'output'],
version: 1,
description: 'Consume Twitter API',
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
defaults: {
name: 'Twitter',
color: '#1DA1F2',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'twitterOAuth1Api',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Tweet',
value: 'tweet',
},
],
default: 'tweet',
description: 'The resource to operate on.',
},
// TWEET
...tweetOperations,
...tweetFields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'tweet') {
// https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
if (operation === 'create') {
const text = this.getNodeParameter('text', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: ITweet = {
status: text,
};
if (additionalFields.attachmentsUi) {
const mediaIds = [];
const attachmentsUi = additionalFields.attachmentsUi as IDataObject;
const uploadUri = 'https://upload.twitter.com/1.1/media/upload.json';
if (attachmentsUi.attachment) {
const attachtments = attachmentsUi.attachment as IDataObject[];
for (const attachment of attachtments) {
const binaryData = items[i].binary as IBinaryKeyData;
const binaryPropertyName = attachment.binaryPropertyName as string;
if (binaryData === undefined) {
throw new Error('No binary data set. So file can not be written!');
}
if (!binaryData[binaryPropertyName]) {
continue;
}
const attachmentBody = {
media_data: binaryData[binaryPropertyName].data,
media_category: snakeCase(attachment.category as string).toUpperCase(),
};
const response = await twitterApiRequest.call(this, 'POST', '', attachmentBody, {}, uploadUri);
mediaIds.push(response.media_id_string);
}
}
body.media_ids = mediaIds.join(',');
}
if (additionalFields.possiblySensitive) {
body.possibly_sensitive = additionalFields.possibly_sensitive as boolean;
}
if (additionalFields.locationFieldsUi) {
const locationUi = additionalFields.locationFieldsUi as IDataObject;
if (locationUi.locationFieldsValues) {
const values = locationUi.locationFieldsValues as IDataObject;
body.lat = parseFloat(values.lalatitude as string);
body.long = parseFloat(values.lalatitude as string);
}
}
responseData = await twitterApiRequest.call(this, 'POST', '/statuses/update.json', body);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -25,7 +25,7 @@ export async function zohoApiRequest(this: IExecuteFunctions | IExecuteSingleFun
};
try {
//@ts-ignore
return await this.helpers.requestOAuth.call(this, 'zohoOAuth2Api', options);
return await this.helpers.requestOAuth2.call(this, 'zohoOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier

View file

@ -93,6 +93,7 @@
"dist/credentials/Msg91Api.credentials.js",
"dist/credentials/MySql.credentials.js",
"dist/credentials/NextCloudApi.credentials.js",
"dist/credentials/OAuth1Api.credentials.js",
"dist/credentials/OAuth2Api.credentials.js",
"dist/credentials/OpenWeatherMapApi.credentials.js",
"dist/credentials/PagerDutyApi.credentials.js",
@ -116,6 +117,7 @@
"dist/credentials/TodoistApi.credentials.js",
"dist/credentials/TrelloApi.credentials.js",
"dist/credentials/TwilioApi.credentials.js",
"dist/credentials/TwitterOAuth1Api.credentials.js",
"dist/credentials/TypeformApi.credentials.js",
"dist/credentials/TogglApi.credentials.js",
"dist/credentials/UpleadApi.credentials.js",
@ -263,6 +265,7 @@
"dist/nodes/Trello/Trello.node.js",
"dist/nodes/Trello/TrelloTrigger.node.js",
"dist/nodes/Twilio/Twilio.node.js",
"dist/nodes/Twitter/Twitter.node.js",
"dist/nodes/Typeform/TypeformTrigger.node.js",
"dist/nodes/Uplead/Uplead.node.js",
"dist/nodes/Vero/Vero.node.js",