From 2ff08657f4f7b5b6cface8c9ec3568741dfa99b8 Mon Sep 17 00:00:00 2001 From: Luke van der Palen Date: Tue, 30 May 2017 17:59:09 +0200 Subject: [PATCH 1/2] Added support for Service Account authentication --- adwords/client.js | 71 +++++++++++++++++++++++++++++++++++++++++ adwords/report.js | 16 +++++----- adwords/service.js | 19 ++++++----- adwords/user.js | 7 ++++- index.js | 1 + readme.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 adwords/client.js diff --git a/adwords/client.js b/adwords/client.js new file mode 100644 index 0000000..441f5c2 --- /dev/null +++ b/adwords/client.js @@ -0,0 +1,71 @@ +'use strict'; +/** + * Adwords user + */ + +const util = require('util'); +const _ = require('lodash'); +const AdwordsServiceDescriptors = require('../services'); +const AdwordsService = require('./service'); +const AdwordsConstants = require('./constants'); + +class AdwordsClient { + + /** + * @inheritDoc + */ + constructor(oAuth2Client, obj) { + this.credentials = _.extend({ + developerToken: '', + userAgent: 'node-adwords', + clientCustomerId: '', + }, obj); + + this.oAuth2Client = oAuth2Client; + } + + /** + * Returns an Api Service Endpoint + * @access public + * @param service {string} the name of the service to load + * @param adwordsversion {string} the adwords version, defaults to 201609 + * @return {AdwordsService} An adwords service object to call methods from + */ + getService(service, adwordsVersion) { + adwordsVersion = adwordsVersion || AdwordsConstants.DEFAULT_ADWORDS_VERSION; + var serviceDescriptor = AdwordsServiceDescriptors[service]; + if (!serviceDescriptor) { + throw new Error( + util.format('No Service Named %s in %s of the adwords api', service, adwordsVersion) + ); + } + + var service = new AdwordsService( + this.oAuth2Client, + this.credentials, + this.populateServiceDescriptor(serviceDescriptor, adwordsVersion) + ); + + return service; + } + + /** + * Populates the service descriptor with dynamic values + * @access protected + * @param serviceDescriptor {object} the obejct from the service descriptor object + * @param adwordsVersion {string} the adwords version to replace inside the service descriptors + * @return {object} a new service descriptor with the proper versioning + */ + populateServiceDescriptor(serviceDescriptor, adwordsVersion) { + var finalServiceDescriptor = _.clone(serviceDescriptor); + for (var index in finalServiceDescriptor) { + if ('string' === typeof finalServiceDescriptor[index]) { + finalServiceDescriptor[index] = finalServiceDescriptor[index].replace(/\{\{version\}\}/g, adwordsVersion); + } + } + return finalServiceDescriptor; + } + +} + +module.exports = AdwordsClient; diff --git a/adwords/report.js b/adwords/report.js index e5d672d..3e651b7 100644 --- a/adwords/report.js +++ b/adwords/report.js @@ -16,9 +16,9 @@ class AdwordsReport { /** * @inheritDoc */ - constructor(credentials) { - this.auth = new AdwordsAuth(credentials); + constructor(oAuthClient) { this.credentials = credentials; + this.oauth2Client = oAuthClient; } /** @@ -52,7 +52,7 @@ class AdwordsReport { if (error || this.reportBodyContainsError(report, body)) { error = error || body; if (-1 !== error.toString().indexOf(AdwordsConstants.OAUTH_ERROR) && retryRequest) { - this.credentials.access_token = null; + this.oauth2Client.credentials.access_token = null; return this.getReport(apiVersion, report, callback, false); } return callback(error, null); @@ -100,16 +100,16 @@ class AdwordsReport { * @param callback {function} */ getAccessToken(callback) { - if (this.credentials.access_token) { - return callback(null, this.credentials.access_token); + if (this.oauth2Client.credentials.access_token) { + return callback(null, this.oauth2Client.credentials.access_token); } - this.auth.refreshAccessToken(this.credentials.refresh_token, (error, tokens) => { + this.oauth2Client.refreshAccessToken(this.oauth2Client.credentials.refresh_token, (error, tokens) => { if (error) { return callback(error); } - this.credentials.access_token = tokens.access_token; - callback(null, this.credentials.access_token); + this.oauth2Client.credentials.access_token = tokens.access_token; + callback(null, this.oauth2Client.credentials.access_token); }); } diff --git a/adwords/service.js b/adwords/service.js index f8fc874..1d03226 100644 --- a/adwords/service.js +++ b/adwords/service.js @@ -16,9 +16,9 @@ class AdwordsService { /** * @inheritDoc */ - constructor(credentials, serviceDescriptor) { + constructor(oAuthClient, credentials, serviceDescriptor) { this.credentials = credentials; - this.auth = new AdwordsAuth(credentials); + this.oauth2Client = oAuthClient; this.serviceDescriptor = serviceDescriptor; this.registerServiceDescriptorMethods(this.serviceDescriptor.methods); } @@ -70,14 +70,13 @@ class AdwordsService { } this.client.setSecurity( - new soap.BearerSecurity(this.credentials.access_token) + new soap.BearerSecurity(this.oauth2Client.credentials.access_token) ); - this.client[method](payload, this.parseResponse((error, response) => { if (error && shouldRetry && -1 !== error.toString().indexOf(AdwordsConstants.OAUTH_ERROR)) { - this.credentials.access_token = null; + this.oauth2Client.this.oauth2Client.credentials.access_token = null; return this.callService(method, payload, callback, false); } callback(error, response); @@ -154,16 +153,16 @@ class AdwordsService { * @param callback {function} */ getAccessToken(callback) { - if (this.credentials.access_token) { - return callback(null, this.credentials.access_token); + if (this.oauth2Client.credentials.access_token) { + return callback(null, this.oauth2Client.credentials.access_token); } - this.auth.refreshAccessToken(this.credentials.refresh_token, (error, tokens) => { + this.oauth2Client.refreshAccessToken(this.oauth2Client.credentials.refresh_token, (error, tokens) => { if (error) { return callback(error); } - this.credentials.access_token = tokens.access_token; - callback(null, this.credentials.access_token); + this.oauth2Client.credentials.access_token = tokens.access_token; + callback(null, this.oauth2Client.credentials.access_token); }); } diff --git a/adwords/user.js b/adwords/user.js index 31d1a9a..3562899 100644 --- a/adwords/user.js +++ b/adwords/user.js @@ -8,6 +8,7 @@ const _ = require('lodash'); const AdwordsServiceDescriptors = require('../services'); const AdwordsService = require('./service'); const AdwordsConstants = require('./constants'); +const AdwordsAuth = require('./auth'); class AdwordsUser { @@ -24,6 +25,9 @@ class AdwordsUser { refresh_token: '',//@todo implement refesh token instead of access token access_token: '', }, obj); + + const auth = new AdwordsAuth( this.credentials ); + this.oAuth2Client = auth.oAuth2Client; } @@ -44,6 +48,7 @@ class AdwordsUser { } var service = new AdwordsService( + this.oAuth2Client, this.credentials, this.populateServiceDescriptor(serviceDescriptor, adwordsVersion) ); @@ -70,4 +75,4 @@ class AdwordsUser { } -module.exports = AdwordsUser; +module.exports = AdwordsUser; \ No newline at end of file diff --git a/index.js b/index.js index f525e44..8354397 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ module.exports = { AdwordsAuth: require('./adwords/auth'), + AdwordsClient: require('./adwords/client'), AdwordsUser: require('./adwords/user'), AdwordsConstants: require('./adwords/constants'), AdwordsReport: require('./adwords/report') diff --git a/readme.md b/readme.md index e31003b..2d86562 100644 --- a/readme.md +++ b/readme.md @@ -161,6 +161,84 @@ app.get('/adwords/auth', (req, res) => { ``` + +## Service account Authentication +How does it work? + +When using Google APIs from the server (or any non-browser based application), authentication is performed through a Service Account, which is a special account representing your application. This account has a unique email address that can be used to grant permissions to. If a user wants to give access to his Google Drive to your application, he must share the files or folders with the Service Account using the supplied email address. + +Now that the Service Account has permission to some user resources, the application can query the API with OAuth2. When using OAuth2, authentication is performed using a token that has been obtained first by submitting a JSON Web Token (JWT). The JWT identifies the user as well as the scope of the data he wants access to. The JWT is also signed with a cryptographic key to prevent tampering. Google generates the key and keeps only the public key for validation. You must keep the private key secure with your application so that you can sign the JWT in order to guarantee its authenticity. + +The application requests a token that can be used for authentication in exchange with a valid JWT. The resulting token can then be used for multiple API calls, until it expires and a new token must be obtained by submitting another JWT. + +Creating a Service Account using the Google Developers Console + +From the Google Developers Console, select your project or create a new one. + +Under "APIs & auth", click "Credentials". + +Under "OAuth", click the "Create new client ID" button. + +Select "Service account" as the application type and click "Create Client ID". + +The key for your new service account should prompt for download automatically. Note that your key is protected with a password. IMPORTANT: keep a secure copy of the key, as Google keeps only the public key. + +Convert the downloaded key to PEM, so we can use it from the Node crypto module. + +To do this, run the following in Terminal: + +`openssl pkcs12 -in downloaded-key-file.p12 -out your-key-file.pem -nodes` + +You will be asked for the password you received during step 5. +hint: notasecret + +That's it! You now have a service account with an email address and a key that you can use from your Node application. + +```js +const AdwordsClient = require('node-adwords').AdwordsClient; +const AdwordsConstants = require('node-adwords').AdwordsConstants; +const googleapis = require('googleapis'); + +const SERVICE_ACCOUNT_KEY_FILE = 'INSERT_PATH_TO_KEYFILE.pem'; + +const jwtClient = new googleapis.auth.JWT( + // use the email address of the service account, as seen in the API console + 'my-service-account@developer.gserviceaccount.com', + // use the PEM file we generated from the downloaded key + SERVICE_ACCOUNT_KEY_FILE, + null, + // specify the scopes you wish to access + ['https://www.googleapis.com/auth/adwords'], + // sub email + 'user-email' +) + +jwtClient.authorize((err) => { + if(err) throw err; + + const client = new AdwordsClient(jwtClient, { + developerToken: 'INSERT_DEVELOPER_TOKEN_HERE', //your adwords developerToken + userAgent: 'INSERT_COMPANY_NAME_HERE', //any company name + clientCustomerId: 'INSERT_CLIENT_CUSTOMER_ID_HERE', //the Adwords Account id (e.g. 123-123-123) + client_id: 'INSERT_OAUTH2_CLIENT_ID_HERE', //this is the api console client_id + }) + + const campaignService = client.getService('CampaignService') + + //create selector + let selector = { + fields: ['Id', 'Name'], + ordering: [{field: 'Name', sortOrder: 'ASCENDING'}], + paging: {startIndex: 0, numberResults: AdwordsConstants.RECOMMENDED_PAGE_SIZE} + } + + campaignService.get({serviceSelector: selector}, (error, result) => { + console.log(error, result); + }) +}) + +``` + # Troubleshooting ## Adwords.Types From 0e35949a0545233c2e467a24611451cc65fa3a08 Mon Sep 17 00:00:00 2001 From: Luke van der Palen Date: Wed, 2 Jan 2019 10:29:42 +0100 Subject: [PATCH 2/2] change package name for private package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8f910a..06963a4 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "node-adwords", + "name": "@freshfruitdigital/node-adwords", "version": "201806.0.1", "description": "An unofficial - feature complete - Adwords sdk for NodeJS", "main": "index.js",