Skip to content

Commit 2d3851c

Browse files
Add API key support to Google-Auth-Library (googleapis#119)
Add fromAPIKey method to jwtClient and supporting logic in oauth2client.js. Applying an API key to an instance of the oauth2client will bypass token checking/refresh and append the given API key as a query string to each request made from the instance in the following form: ```JS `{target_base_url}?key={given_key}` ```
1 parent 48aa6ea commit 2d3851c

File tree

7 files changed

+182
-5
lines changed

7 files changed

+182
-5
lines changed

lib/auth/googleauth.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,22 @@ GoogleAuth.prototype.fromStream = function(stream, opt_callback) {
516516
});
517517
};
518518

519+
/**
520+
* Create a credentials instance using the given API key string.
521+
* @param {string} - The API key string
522+
* @param {function=} - Optional callback function
523+
*/
524+
GoogleAuth.prototype.fromAPIKey = function (apiKey, opt_callback) {
525+
var client = new this.JWTClient();
526+
client.fromAPIKey(apiKey, function (err) {
527+
if (err) {
528+
callback(opt_callback, err);
529+
} else {
530+
callback(opt_callback, null, client);
531+
}
532+
});
533+
};
534+
519535
/**
520536
* Determines whether the current operating system is Windows.
521537
* @api private

lib/auth/jwtclient.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var Auth2Client = require('./oauth2client.js');
2020
var gToken = require('gtoken');
2121
var JWTAccess = require('./jwtaccess.js');
2222
var noop = require('lodash.noop');
23+
var isString = require('lodash.isstring');
2324
var util = require('util');
2425

2526

@@ -207,6 +208,25 @@ JWT.prototype.fromStream = function(stream, opt_callback) {
207208
});
208209
};
209210

211+
/**
212+
* Creates a JWT credentials instance using an API Key for authentication.
213+
* @param {string} apiKey - the API Key in string form.
214+
* @param {function=} opt_callback - Optional callback to be invoked after
215+
* initialization.
216+
*/
217+
JWT.prototype.fromAPIKey = function (apiKey, opt_callback) {
218+
var done = opt_callback || noop;
219+
220+
if (!isString(apiKey)) {
221+
setImmediate(function () {
222+
done(new Error('Must provide an API Key string.'));
223+
});
224+
return;
225+
}
226+
this.setAPIKey(apiKey);
227+
done(null);
228+
};
229+
210230
/**
211231
* Creates the gToken instance if it has not been created already.
212232
* @param {function=} callback Callback.

lib/auth/oauth2client.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var noop = require('lodash.noop');
2222
var PemVerifier = require('./../pemverifier.js');
2323
var querystring = require('querystring');
2424
var util = require('util');
25+
var merge = require('lodash.merge');
2526

2627
var certificateCache = null;
2728
var certificateExpiry = null;
@@ -43,6 +44,7 @@ function OAuth2Client(clientId, clientSecret, redirectUri, opt_opts) {
4344
this.redirectUri_ = redirectUri;
4445
this.opts = opt_opts || {};
4546
this.credentials = {};
47+
this.apiKey = null;
4648
}
4749

4850
/**
@@ -267,8 +269,9 @@ OAuth2Client.prototype.getRequestMetadata = function(opt_uri, metadataCb) {
267269
var that = this;
268270
var thisCreds = this.credentials;
269271

270-
if (!thisCreds.access_token && !thisCreds.refresh_token) {
271-
return metadataCb(new Error('No access or refresh token is set.'), null);
272+
if (!thisCreds.access_token && !thisCreds.refresh_token && !this.apiKey) {
273+
return metadataCb(new Error('No access, refresh token or API key is set.'),
274+
null);
272275
}
273276

274277
// if no expiry time, assume it's not expired
@@ -278,7 +281,11 @@ OAuth2Client.prototype.getRequestMetadata = function(opt_uri, metadataCb) {
278281
if (thisCreds.access_token && !isTokenExpired) {
279282
thisCreds.token_type = thisCreds.token_type || 'Bearer';
280283
var headers = {'Authorization': thisCreds.token_type + ' ' + thisCreds.access_token };
281-
return metadataCb(null, headers , null);
284+
return metadataCb(null, headers, null);
285+
}
286+
287+
if (this.apiKey) {
288+
return metadataCb(null, {}, null);
282289
}
283290

284291
return this.refreshToken_(thisCreds.refresh_token, function(err, tokens, response) {
@@ -299,6 +306,15 @@ OAuth2Client.prototype.getRequestMetadata = function(opt_uri, metadataCb) {
299306
});
300307
};
301308

309+
/**
310+
* Sets the given value as the value of the apiKey
311+
* property on the client instance.
312+
* @param {string} apiKey - the apiKey to use for authentication
313+
*/
314+
OAuth2Client.prototype.setAPIKey = function (apiKey) {
315+
this.apiKey = apiKey;
316+
};
317+
302318
/**
303319
* Revokes the access given to token.
304320
* @param {string} token The existing token to be revoked.
@@ -371,6 +387,13 @@ OAuth2Client.prototype.request = function(opts, callback) {
371387
opts.headers = opts.headers || {};
372388
opts.headers.Authorization = headers.Authorization;
373389
}
390+
if (that.apiKey) {
391+
if (opts.qs) {
392+
opts.qs = merge({}, opts.qs, {key: that.apiKey});
393+
} else {
394+
opts.qs = {key: that.apiKey};
395+
}
396+
}
374397
return that._makeRequest(opts, postRequestCb);
375398
}
376399
};

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
],
2121
"dependencies": {
2222
"gtoken": "^1.2.1",
23-
"lodash.noop": "^3.0.1",
2423
"jws": "^3.1.4",
24+
"lodash.isstring": "^4.0.1",
25+
"lodash.merge": "^4.6.0",
26+
"lodash.noop": "^3.0.1",
2527
"request": "^2.74.0"
2628
},
2729
"devDependencies": {

test/test.googleauth.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,89 @@ describe('GoogleAuth', function() {
129129
});
130130
});
131131

132+
describe('.fromAPIKey', function () {
133+
var API_KEY = 'test-123';
134+
var STUB_PROJECT = 'my-awesome-project';
135+
describe('Exception behaviour', function () {
136+
var auth;
137+
before(function () {
138+
auth = new GoogleAuth();
139+
});
140+
it('Should error given an invalid api key', function (done) {
141+
auth.fromAPIKey(null, function (err) {
142+
assert(err instanceof Error);
143+
done();
144+
});
145+
});
146+
});
147+
describe('Request/response lifecycle mocking', function () {
148+
var ENDPOINT = '/events:report';
149+
var RESPONSE_BODY = 'RESPONSE_BODY';
150+
var BASE_URL = [
151+
'https://clouderrorreporting.googleapis.com/v1beta1/projects',
152+
STUB_PROJECT
153+
].join('/');
154+
var auth;
155+
beforeEach(function () {
156+
auth = new GoogleAuth();
157+
insertEnvironmentVariableIntoAuth(auth, 'GCLOUD_PROJECT', STUB_PROJECT);
158+
});
159+
afterEach(function () {
160+
nock.cleanAll();
161+
});
162+
describe('With no added query string parameters', function () {
163+
it('should make a request with the api key', function (done) {
164+
var fakeService = nock(BASE_URL).post(ENDPOINT)
165+
.query({key: API_KEY}).once().reply(function (uri) {
166+
assert(uri.indexOf('key='+API_KEY) > -1);
167+
return [200, RESPONSE_BODY];
168+
});
169+
auth.fromAPIKey(API_KEY, function (err, client) {
170+
assert.strictEqual(err, null);
171+
client.request({
172+
url: BASE_URL+ENDPOINT,
173+
method: 'POST',
174+
json: '{"test": true}'
175+
}, function (err, body) {
176+
assert.strictEqual(err, null);
177+
assert.strictEqual(RESPONSE_BODY, body);
178+
fakeService.done();
179+
done();
180+
});
181+
});
182+
});
183+
});
184+
describe('With preexisting query string parameters', function () {
185+
it('should make a request while preserving original parameters',
186+
function (done) {
187+
var OTHER_QS_PARAM = {test: 'abc'};
188+
var fakeService = nock(BASE_URL).post(ENDPOINT)
189+
.query({test: OTHER_QS_PARAM.test, key: API_KEY}).once()
190+
.reply(function (uri) {
191+
assert(uri.indexOf('key='+API_KEY) > -1);
192+
assert(uri.indexOf('test='+OTHER_QS_PARAM.test) > -1);
193+
return [200, RESPONSE_BODY];
194+
});
195+
auth.fromAPIKey(API_KEY, function (err, client) {
196+
assert.strictEqual(err, null);
197+
client.request({
198+
url: BASE_URL+ENDPOINT,
199+
method: 'POST',
200+
json: '{"test": true}',
201+
qs: OTHER_QS_PARAM
202+
}, function (err, body) {
203+
assert.strictEqual(err, null);
204+
assert.strictEqual(RESPONSE_BODY, body);
205+
fakeService.done();
206+
done();
207+
});
208+
});
209+
}
210+
);
211+
});
212+
});
213+
});
214+
132215
describe('JWT token', function() {
133216

134217
it('should error on empty json', function (done) {

test/test.jwt.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,3 +686,36 @@ describe('.fromStream', function () {
686686
});
687687

688688
});
689+
690+
describe('.fromAPIKey', function () {
691+
var jwt;
692+
var KEY = 'test';
693+
beforeEach(function () {
694+
var auth = new GoogleAuth();
695+
jwt = new auth.JWT();
696+
});
697+
describe('exception behaviour', function () {
698+
it('should error without api key', function (done) {
699+
jwt.fromAPIKey(undefined, function (err) {
700+
assert(err instanceof Error);
701+
done();
702+
});
703+
});
704+
it('should error with invalid api key type', function (done) {
705+
jwt.fromAPIKey({key: KEY}, function (err) {
706+
assert(err instanceof Error);
707+
done();
708+
});
709+
});
710+
});
711+
describe('Valid behaviour', function () {
712+
713+
it('should set the .apiKey property on the instance', function (done) {
714+
jwt.fromAPIKey(KEY, function (err) {
715+
assert.strictEqual(jwt.apiKey, KEY);
716+
assert.strictEqual(err, null);
717+
done();
718+
});
719+
});
720+
});
721+
});

test/test.oauth2.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ describe('OAuth2 client', function() {
948948
var auth = new GoogleAuth();
949949
var oauth2client = new auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
950950
oauth2client.request({}, function(err, result) {
951-
assert.equal(err.message, 'No access or refresh token is set.');
951+
assert.equal(err.message, 'No access, refresh token or API key is set.');
952952
assert.equal(result, null);
953953
done();
954954
});

0 commit comments

Comments
 (0)