Skip to content

Added support for Service Account authentication #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions adwords/client.js
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 8 additions & 8 deletions adwords/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class AdwordsReport {
/**
* @inheritDoc
*/
constructor(credentials) {
this.auth = new AdwordsAuth(credentials);
constructor(oAuthClient) {
this.credentials = credentials;
this.oauth2Client = oAuthClient;
}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
}

Expand Down
19 changes: 9 additions & 10 deletions adwords/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
}

Expand Down
7 changes: 6 additions & 1 deletion adwords/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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;
}


Expand All @@ -44,6 +48,7 @@ class AdwordsUser {
}

var service = new AdwordsService(
this.oAuth2Client,
this.credentials,
this.populateServiceDescriptor(serviceDescriptor, adwordsVersion)
);
Expand All @@ -70,4 +75,4 @@ class AdwordsUser {

}

module.exports = AdwordsUser;
module.exports = AdwordsUser;
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
78 changes: 78 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down