diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f5a608331a..0a93f18e34 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -2,6 +2,7 @@ import * as express from 'express'; import { dirname as pathDirname, join as pathJoin, + resolve as pathResolve, } from 'path'; import { getConnectionManager, @@ -850,7 +851,7 @@ class App { // ---------------------------------------- - // Returns all the credential types which are defined in the loaded n8n-modules + // Authorize OAuth Data this.app.get('/rest/oauth2-credential/auth', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { if (req.query.id === undefined) { throw new Error('Required credential id is missing!'); @@ -877,13 +878,13 @@ class App { throw new Error('Unable to read OAuth credentials'); } - let token = new csrf(); + const token = new csrf(); // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR oauthCredentials.csrfSecret = token.secretSync(); const state = { - 'token': token.create(oauthCredentials.csrfSecret), - 'cid': req.query.id - } + token: token.create(oauthCredentials.csrfSecret), + cid: req.query.id + }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; const oAuthObj = new clientOAuth2({ @@ -891,9 +892,9 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: _.get(oauthCredentials, 'callbackUrl', WebhookHelpers.getWebhookBaseUrl()) as string, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ','), - state: stateEncodedStr + state: stateEncodedStr, }); credentials.setData(oauthCredentials, encryptionKey); @@ -913,42 +914,46 @@ class App { // ---------------------------------------- // Verify and store app code. Generate access tokens and store for respective credential. - this.app.get('/rest/oauth2-credential/callback', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { + this.app.get('/rest/oauth2-credential/callback', async (req: express.Request, res: express.Response) => { const {code, state: stateEncoded} = req.query; + if (code === undefined || stateEncoded === undefined) { - throw new Error('Insufficient parameters for OAuth2 callback') + throw new Error('Insufficient parameters for OAuth2 callback'); } let state; try { state = JSON.parse(Buffer.from(stateEncoded, 'base64').toString()); } catch (error) { - throw new Error('Invalid state format returned'); + const errorResponse = new ResponseHelper.ResponseError('Invalid state format returned', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const result = await Db.collections.Credentials!.findOne(state.cid); if (result === undefined) { - res.status(404).send('The credential is not known.'); - return ''; + 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) { - throw new Error('No encryption key got found to decrypt the credentials!'); + const errorResponse = new ResponseHelper.ResponseError('No encryption key got found to decrypt the credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); const oauthCredentials = (result as ICredentialsDecryptedDb).data; if (oauthCredentials === undefined) { - throw new Error('Unable to read OAuth credentials'); + const errorResponse = new ResponseHelper.ResponseError('Unable to read OAuth credentials!', undefined, 503); + return ResponseHelper.sendErrorResponse(res, errorResponse); } - let token = new csrf(); + const token = new csrf(); if (oauthCredentials.csrfSecret === undefined || !token.verify(oauthCredentials.csrfSecret as string, state.token)) { - res.status(404).send('The OAuth2 callback state is invalid.'); - return ''; + const errorResponse = new ResponseHelper.ResponseError('The OAuth2 callback state is invalid!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); } const oAuthObj = new clientOAuth2({ @@ -956,13 +961,15 @@ class App { clientSecret: _.get(oauthCredentials, 'clientSecret', '') as string, accessTokenUri: _.get(oauthCredentials, 'accessTokenUrl', '') as string, authorizationUri: _.get(oauthCredentials, 'authUrl', '') as string, - redirectUri: _.get(oauthCredentials, 'callbackUrl', WebhookHelpers.getWebhookBaseUrl()) as string, + redirectUri: `${WebhookHelpers.getWebhookBaseUrl()}rest/oauth2-credential/callback`, scopes: _.split(_.get(oauthCredentials, 'scope', 'openid,') as string, ',') }); const oauthToken = await oAuthObj.code.getToken(req.originalUrl); + if (oauthToken === undefined) { - throw new Error('Unable to get access tokens'); + const errorResponse = new ResponseHelper.ResponseError('Unable to get access tokens!', undefined, 404); + return ResponseHelper.sendErrorResponse(res, errorResponse); } oauthCredentials.oauthTokenData = JSON.stringify(oauthToken.data); @@ -974,8 +981,9 @@ class App { // Save the credentials in DB await Db.collections.Credentials!.update(state.cid, newCredentialsData); - return 'Success!'; - })); + res.sendFile(pathResolve('templates/oauth-callback.html')); + }); + // ---------------------------------------- // Executions diff --git a/packages/cli/templates/oauth-callback.html b/packages/cli/templates/oauth-callback.html new file mode 100644 index 0000000000..e479c5ea9e --- /dev/null +++ b/packages/cli/templates/oauth-callback.html @@ -0,0 +1,9 @@ + + + +Got connected. The window can be closed now. + diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a79ebdd8fd..7c692881a8 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -145,8 +145,8 @@ export interface IRestApi { deleteExecutions(sendData: IExecutionDeleteFilter): Promise; retryExecution(id: string, loadWorkflow?: boolean): Promise; getTimezones(): Promise; - OAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; - OAuth2Callback(code: string, state: string): Promise; + oAuth2CredentialAuthorize(sendData: ICredentialsResponse): Promise; + oAuth2Callback(code: string, state: string): Promise; } export interface IBinaryDisplayData { diff --git a/packages/editor-ui/src/components/CredentialsInput.vue b/packages/editor-ui/src/components/CredentialsInput.vue index 0830b2a2a0..f7616f4706 100644 --- a/packages/editor-ui/src/components/CredentialsInput.vue +++ b/packages/editor-ui/src/components/CredentialsInput.vue @@ -13,6 +13,26 @@ + + + OAuth + + + + + + + Is connected + + + + + + Is NOT connected + + + +
Credential Data: @@ -152,6 +172,16 @@ export default mixins( }; }); }, + isOAuthType (): boolean { + return this.credentialData && this.credentialData.type === 'oAuth2Api'; + }, + isOAuthConnected (): boolean { + if (this.isOAuthType === false) { + return false; + } + + return !!this.credentialData.data.oauthTokenData; + }, }, methods: { valueChanged (parameterData: IUpdateInformation) { @@ -189,6 +219,48 @@ export default mixins( this.$emit('credentialsCreated', result); }, + async oAuth2CredentialAuthorize () { + let url; + try { + url = await this.restApi().oAuth2CredentialAuthorize(this.credentialData) as string; + } catch (error) { + this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); + return; + } + + const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`; + const oauthPopup = window.open(url, 'OAuth2 Authorization', params); + + const receiveMessage = (event: MessageEvent) => { + // // TODO: Add check that it came from n8n + // if (event.origin !== 'http://example.org:8080') { + // return; + // } + + if (event.data === 'success') { + + // Set some kind of data that status changes. + // As data does not get displayed directly it does not matter what data. + this.credentialData.data.oauthTokenData = {}; + + // Close the window + if (oauthPopup) { + oauthPopup.close(); + } + + this.$showMessage({ + title: 'Connected', + message: 'Got connected!', + type: 'success', + }); + } + + // Make sure that the event gets removed again + window.removeEventListener('message', receiveMessage, false); + }; + + window.addEventListener('message', receiveMessage, false); + }, async updateCredentials () { const nodesAccess: ICredentialNodeAccess[] = []; const addedNodeTypes: string[] = []; @@ -301,6 +373,11 @@ export default mixins( line-height: 1.75em; } + .oauth-information { + line-height: 2.5em; + margin-top: 2em; + } + .parameter-wrapper { line-height: 3em; diff --git a/packages/editor-ui/src/components/CredentialsList.vue b/packages/editor-ui/src/components/CredentialsList.vue index eeece990bc..c1d3ac93ff 100644 --- a/packages/editor-ui/src/components/CredentialsList.vue +++ b/packages/editor-ui/src/components/CredentialsList.vue @@ -25,12 +25,10 @@ + width="120"> @@ -93,20 +91,6 @@ export default mixins( this.editCredentials = null; this.credentialEditDialogVisible = true; }, - async OAuth2CredentialAuthorize (credential: ICredentialsResponse) { - let url; - try { - url = await this.restApi().OAuth2CredentialAuthorize(credential) as string; - } catch (error) { - this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); - return; - } - - const params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,width=0,height=0,left=-1000,top=-1000`; - const oauthPopup = window.open(url, 'OAuth2 Authorization', params); - - console.log(oauthPopup); - }, editCredential (credential: ICredentialsResponse) { const editCredentials = { id: credential.id, diff --git a/packages/editor-ui/src/components/mixins/restApi.ts b/packages/editor-ui/src/components/mixins/restApi.ts index a2cdbd6584..be114786a9 100644 --- a/packages/editor-ui/src/components/mixins/restApi.ts +++ b/packages/editor-ui/src/components/mixins/restApi.ts @@ -253,15 +253,15 @@ export const restApi = Vue.extend({ }, // Get OAuth2 Authorization URL using the stored credentials - OAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { + oAuth2CredentialAuthorize: (sendData: ICredentialsResponse): Promise => { return self.restApi().makeRestApiRequest('GET', `/oauth2-credential/auth`, sendData); }, // Verify OAuth2 provider callback and kick off token generation - OAuth2Callback: (code: string, state: string): Promise => { + oAuth2Callback: (code: string, state: string): Promise => { const sendData = { 'code': code, - 'state': state + 'state': state, }; return self.restApi().makeRestApiRequest('POST', `/oauth2-credential/callback`, sendData); diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index f9a7f879a0..0e6018f3aa 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -71,6 +71,7 @@ import { faSave, faSearchMinus, faSearchPlus, + faSignInAlt, faSlidersH, faSpinner, faStop, @@ -145,6 +146,7 @@ library.add(faRss); library.add(faSave); library.add(faSearchMinus); library.add(faSearchPlus); +library.add(faSignInAlt); library.add(faSlidersH); library.add(faSpinner); library.add(faStop); diff --git a/packages/editor-ui/src/router.ts b/packages/editor-ui/src/router.ts index f33d028f94..e82b30b588 100644 --- a/packages/editor-ui/src/router.ts +++ b/packages/editor-ui/src/router.ts @@ -21,7 +21,7 @@ export default new Router({ }, { path: '/oauth2/callback', - name: 'OAuth2Callback', + name: 'oAuth2Callback', components: { }, }, diff --git a/packages/nodes-base/credentials/OAuth2Api.credentials.ts b/packages/nodes-base/credentials/OAuth2Api.credentials.ts index 452fdb8f57..99e8e025de 100644 --- a/packages/nodes-base/credentials/OAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/OAuth2Api.credentials.ts @@ -1,56 +1,49 @@ import { - ICredentialType, - NodePropertyTypes, + ICredentialType, + NodePropertyTypes, } from 'n8n-workflow'; export class OAuth2Api implements ICredentialType { - name = 'OAuth2Api'; - displayName = 'OAuth2 API'; - properties = [ - { - 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: 'Callback URL', - name: 'callbackUrl', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Client ID', - name: 'clientId', - type: 'string' as NodePropertyTypes, - default: '', - required: true, - }, - { - displayName: 'Client Secret', - name: 'clientSecret', - type: 'string' as NodePropertyTypes, - typeOptions: { - password: true, - }, - default: '', - required: true, - }, - { - displayName: 'Scope', - name: 'scope', - type: 'string' as NodePropertyTypes, - default: '', - }, - ]; + name = 'oAuth2Api'; + displayName = 'OAuth2 API'; + properties = [ + { + 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: 'Client ID', + name: 'clientId', + type: 'string' as NodePropertyTypes, + default: '', + required: true, + }, + { + displayName: 'Client Secret', + name: 'clientSecret', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; } diff --git a/packages/nodes-base/nodes/OAuth.node.ts b/packages/nodes-base/nodes/OAuth.node.ts index 189ae9e408..bf2f193a2f 100644 --- a/packages/nodes-base/nodes/OAuth.node.ts +++ b/packages/nodes-base/nodes/OAuth.node.ts @@ -1,104 +1,69 @@ import { IExecuteFunctions } from 'n8n-core'; import { - GenericValue, - IDataObject, - INodeExecutionData, - INodeType, - INodeTypeDescription, + INodeExecutionData, + INodeType, + INodeTypeDescription, } from 'n8n-workflow'; -import { set } from 'lodash'; - -import * as util from 'util'; -import { connectionFields } from './ActiveCampaign/ConnectionDescription'; - export class OAuth implements INodeType { - description: INodeTypeDescription = { - displayName: 'OAuth', - name: 'oauth', + description: INodeTypeDescription = { + displayName: 'OAuth', + name: 'oauth', icon: 'fa:code-branch', - group: ['input'], - version: 1, - description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', - defaults: { - name: 'OAuth', - color: '#0033AA', - }, - inputs: ['main'], - outputs: ['main'], - credentials: [ - { - name: 'OAuth2Api', - required: true, - } - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - options: [ - { - name: 'Get', - value: 'get', - description: 'Returns the value of a key from oauth.', - }, - ], - default: 'get', - description: 'The operation to perform.', - }, + group: ['input'], + version: 1, + description: 'Gets, sends data to Oauth API Endpoint and receives generic information.', + defaults: { + name: 'OAuth', + color: '#0033AA', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'oAuth2Api', + required: true, + } + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + description: 'Returns the OAuth token data.', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, - // ---------------------------------- - // get - // ---------------------------------- - { - displayName: 'Name', - name: 'propertyName', - type: 'string', - displayOptions: { - show: { - operation: [ - 'get' - ], - }, - }, - default: 'propertyName', - required: true, - description: 'Name of the property to write received data to.
Supports dot-notation.
Example: "data.person[0].name"', - }, - ] - }; + ] + }; - async execute(this: IExecuteFunctions): Promise { - const credentials = this.getCredentials('OAuth2Api'); - if (credentials === undefined) { - throw new Error('No credentials got returned!'); - } + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('oAuth2Api'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } - if (credentials.oauthTokenData === undefined) { - throw new Error('OAuth credentials not connected'); - } + if (credentials.oauthTokenData === undefined) { + throw new Error('OAuth credentials not connected'); + } - const operation = this.getNodeParameter('operation', 0) as string; - if (operation === 'get') { - const items = this.getInputData(); - const returnItems: INodeExecutionData[] = []; + const operation = this.getNodeParameter('operation', 0) as string; + if (operation === 'get') { + // credentials.oauthTokenData has the refreshToken and accessToken available + // it would be nice to have credentials.getOAuthToken() which returns the accessToken + // and also handles an error case where if the token is to be refreshed, it does so + // without knowledge of the node. - let item: INodeExecutionData; - - // credentials.oauthTokenData has the refreshToken and accessToken available - // it would be nice to have credentials.getOAuthToken() which returns the accessToken - // and also handles an error case where if the token is to be refreshed, it does so - // without knowledge of the node. - console.log('Got OAuth credentials!', credentials.oauthTokenData); - - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - item = { json: { itemIndex } }; - returnItems.push(item); - } - return [returnItems]; - } else { - throw new Error('Unknown operation'); - } - } + return [this.helpers.returnJsonArray(JSON.parse(credentials.oauthTokenData as string))]; + } else { + throw new Error('Unknown operation'); + } + } }