diff --git a/packages/nodes-base/credentials/GoogleFirebaseApi.credentials.ts b/packages/nodes-base/credentials/GoogleFirebaseApi.credentials.ts new file mode 100644 index 0000000000..1191491e8a --- /dev/null +++ b/packages/nodes-base/credentials/GoogleFirebaseApi.credentials.ts @@ -0,0 +1,27 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GoogleFirebaseApi implements ICredentialType { + name = 'googleFirebaseApi'; + displayName = 'Google Firebase API'; + documentationUrl = 'google'; + properties = [ + { + displayName: 'Service Account Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.
See the tutorial on how to create one.', + }, + { + displayName: 'Private Key', + name: 'privateKey', + lines: 5, + type: 'string' as NodePropertyTypes, + default: '', + description: 'Use the multiline editor. Make sure there are exactly 3 lines.
-----BEGIN PRIVATE KEY-----
KEY IN A SINGLE LINE
-----END PRIVATE KEY-----', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.ts index ef1d63b283..3a3a49ebaf 100644 --- a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.ts +++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/CloudFirestore.node.ts @@ -44,12 +44,46 @@ export class CloudFirestore implements INodeType { inputs: ['main'], outputs: ['main'], credentials: [ + { + name: 'googleFirebaseApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, { name: 'googleFirebaseCloudFirestoreOAuth2Api', required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Service Account', + value: 'serviceAccount', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'oAuth2', + }, { displayName: 'Resource', name: 'resource', diff --git a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GenericFunctions.ts index f78bbaa824..74490aad55 100644 --- a/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Firebase/CloudFirestore/GenericFunctions.ts @@ -12,7 +12,12 @@ import { IDataObject, } from 'n8n-workflow'; +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri: string | null = null): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; const options: OptionsWithUri = { headers: { @@ -32,8 +37,22 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF delete options.body; } - //@ts-ignore - return await this.helpers.requestOAuth2.call(this, 'googleFirebaseCloudFirestoreOAuth2Api', options); + if (authenticationMethod === 'serviceAccount') { + const credentials = this.getCredentials('googleFirebaseApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const { access_token } = await getAccessToken.call(this, credentials as IDataObject); + + options.headers!.Authorization = `Bearer ${access_token}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'googleFirebaseCloudFirestoreOAuth2Api', options); + } } catch (error) { let errors; @@ -152,3 +171,51 @@ export function documentToJson(fields: IDataObject): IDataObject { } return result; } + +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { + //https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/datastore', + 'https://www.googleapis.com/auth/firebase', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + 'iss': credentials.email as string, + 'sub': credentials.delegatedEmail || credentials.email as string, + 'scope': scopes.join(' '), + 'aud': `https://oauth2.googleapis.com/token`, + 'iat': now, + 'exp': now + 3600, + }, + credentials.privateKey as string, + { + algorithm: 'RS256', + header: { + 'kid': credentials.privateKey as string, + 'typ': 'JWT', + 'alg': 'RS256', + }, + }, + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: 'https://oauth2.googleapis.com/token', + json: true, + }; + + //@ts-ignore + return this.helpers.request(options); +} + diff --git a/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/GenericFunctions.ts index 66a38a0666..1a178a0170 100644 --- a/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/GenericFunctions.ts @@ -1,4 +1,5 @@ import { + OptionsWithUri, OptionsWithUrl, } from 'request'; @@ -12,7 +13,12 @@ import { IDataObject, } from 'n8n-workflow'; +import * as moment from 'moment-timezone'; + +import * as jwt from 'jsonwebtoken'; + export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, projectId: string, method: string, resource: string, body: any = {}, qs: IDataObject = {}, headers: IDataObject = {}, uri: string | null = null): Promise { // tslint:disable-line:no-any + const authenticationMethod = this.getNodeParameter('authentication', 0, 'serviceAccount') as string; const options: OptionsWithUrl = { headers: { @@ -32,7 +38,22 @@ export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleF delete options.body; } - return await this.helpers.requestOAuth2!.call(this, 'googleFirebaseRealtimeDatabaseOAuth2Api', options); + if (authenticationMethod === 'serviceAccount') { + const credentials = this.getCredentials('googleFirebaseApi'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const { access_token } = await getAccessToken.call(this, credentials as IDataObject); + + options.headers!.Authorization = `Bearer ${access_token}`; + //@ts-ignore + return await this.helpers.request(options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'googleFirebaseRealtimeDatabaseOAuth2Api', options); + } } catch (error) { if (error.response && error.response.body && error.response.body.error) { @@ -76,3 +97,51 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | IExecut return returnData; } + +function getAccessToken(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, credentials: IDataObject): Promise { + //https://developers.google.com/identity/protocols/oauth2/service-account#httprest + + const scopes = [ + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/firebase.database', + 'https://www.googleapis.com/auth/firebase', + ]; + + const now = moment().unix(); + + const signature = jwt.sign( + { + 'iss': credentials.email as string, + 'sub': credentials.delegatedEmail || credentials.email as string, + 'scope': scopes.join(' '), + 'aud': `https://oauth2.googleapis.com/token`, + 'iat': now, + 'exp': now + 3600, + }, + credentials.privateKey as string, + { + algorithm: 'RS256', + header: { + 'kid': credentials.privateKey as string, + 'typ': 'JWT', + 'alg': 'RS256', + }, + }, + ); + + const options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + form: { + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: signature, + }, + uri: 'https://oauth2.googleapis.com/token', + json: true, + }; + + //@ts-ignore + return this.helpers.request(options); +} diff --git a/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.ts b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.ts index 38eaac6d0c..037a2ac55b 100644 --- a/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.ts +++ b/packages/nodes-base/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.ts @@ -32,11 +32,45 @@ export class RealtimeDatabase implements INodeType { inputs: ['main'], outputs: ['main'], credentials: [ + { + name: 'googleFirebaseApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'serviceAccount', + ], + }, + }, + }, { name: 'googleFirebaseRealtimeDatabaseOAuth2Api', + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, ], properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Service Account', + value: 'serviceAccount', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'oAuth2', + }, { displayName: 'Project ID', name: 'projectId', diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 1ace196314..4613d504b4 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -92,6 +92,7 @@ "dist/credentials/GoogleContactsOAuth2Api.credentials.js", "dist/credentials/GoogleCloudNaturalLanguageOAuth2Api.credentials.js", "dist/credentials/GoogleDriveOAuth2Api.credentials.js", + "dist/credentials/GoogleFirebaseApi.credentials.js", "dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js", "dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js",