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 4b48347..67eb1aa 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); @@ -103,16 +103,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 459e910..6ca8fd8 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); } @@ -69,14 +69,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); @@ -153,16 +152,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 d8b3bec..1366c91 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/package.json b/package.json index 88bbf21..34e091e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "node-adwords", + "name": "@freshfruitdigital/node-adwords", "version": "201809.0.1", "description": "An unofficial - feature complete - Adwords sdk for NodeJS", "main": "index.js", diff --git a/readme.md b/readme.md index 6c2bc73..087d382 100644 --- a/readme.md +++ b/readme.md @@ -154,6 +154,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