From e63e36d457cb913cd8126d046d9bd3963c602518 Mon Sep 17 00:00:00 2001 From: Stephen Sawchuk Date: Tue, 25 Aug 2015 22:31:30 -0400 Subject: [PATCH] core: use google-auto-auth --- lib/common/util.js | 141 ++------------- package.json | 2 +- test/common/util.js | 419 ++++++-------------------------------------- 3 files changed, 67 insertions(+), 495 deletions(-) diff --git a/lib/common/util.js b/lib/common/util.js index 0943bc75174..a66433fafd5 100644 --- a/lib/common/util.js +++ b/lib/common/util.js @@ -22,7 +22,7 @@ */ var extend = require('extend'); -var GoogleAuth = require('google-auth-library'); +var googleAuth = require('google-auto-auth'); var is = require('is'); var nodeutil = require('util'); var request = require('request').defaults({ @@ -324,109 +324,6 @@ function shouldRetryRequest(err) { util.shouldRetryRequest = shouldRetryRequest; -/** - * Create an Auth Client from Google Auth Library, used to get an access token - * for authenticating API requests. - * - * @param {object} config - Configuration object. - * @param {object=} config.authClient - AuthClient object. If not provided, - * it will be created and cached here. - * @param {object=} config.credentials - Credentials object. - * @param {string=} config.email - Account email address, required for PEM/P12 - * usage. - * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile. - * @param {array} config.scopes - Array of scopes required for the API. - * @param {function} callback - The callback function. - */ -function getAuthClient(config, callback) { - if (config.authClient) { - setImmediate(function() { - callback(null, config.authClient); - }); - return; - } - var googleAuth = new GoogleAuth(); - - if (config.keyFile) { - var authClient = new googleAuth.JWT(); - authClient.keyFile = config.keyFile; - authClient.email = config.email; - authClient.scopes = config.scopes; - addScope(null, authClient); - } else if (config.credentials) { - googleAuth.fromJSON(config.credentials, addScope); - } else { - googleAuth.getApplicationDefault(addScope); - } - - function addScope(err, authClient) { - if (err) { - callback(err); - return; - } - - if (authClient.createScopedRequired && authClient.createScopedRequired()) { - authClient = authClient.createScoped(config.scopes); - } - - config.authClient = authClient; - callback(null, authClient); - } -} - -util.getAuthClient = getAuthClient; - -/** - * Authenticate a request by extending its headers object with an access token. - * - * @param {object} config - Configuration object. - * @param {object=} config.authClient - AuthClient object. If not provided, - * it will be created and cached here. - * @param {object=} config.credentials - Credentials object. - * @param {string=} config.email - Account email address, required for PEM/P12 - * usage. - * @param {string=} config.keyFile - Path to a .json, .pem, or .p12 keyfile. - * @param {array} config.scopes - Array of scopes required for the API. - * @param {object} reqOpts - HTTP request options. Its `headers` object is - * created or extended with a valid access token. - * @param {function} callback - The callback function. - */ -function authorizeRequest(config, reqOpts, callback) { - util.getAuthClient(config, function(err, authClient) { - if (err) { - // google-auth-library returns a "Could not load..." error if it can't get - // an access token. However, it's possible an API request doesn't need to - // be authenticated, e.g. when downloading a file from a public bucket. We - // consider this error a warning, and allow the request to go through - // without authorization, relying on the upstream API to return an error - // the user would find more helpful, should one occur. - if (err.message.indexOf('Could not load') === 0) { - callback(null, reqOpts); - } else { - callback(err); - } - return; - } - - authClient.getAccessToken(function(err, token) { - if (err) { - callback(err); - return; - } - - var authorizedReqOpts = extend(true, {}, reqOpts, { - headers: { - Authorization: 'Bearer ' + token - } - }); - - callback(null, authorizedReqOpts); - }); - }); -} - -util.authorizeRequest = authorizeRequest; - /** * Get a function for making authorized requests. * @@ -450,6 +347,8 @@ util.authorizeRequest = authorizeRequest; function makeAuthorizedRequestFactory(config) { config = config || {}; + var authClient = googleAuth(config); + /** * The returned function that will make an authorized request. * @@ -470,7 +369,13 @@ function makeAuthorizedRequestFactory(config) { } function onAuthorized(err, authorizedReqOpts) { - if (err) { + // google-auth-library returns a "Could not load..." error if it can't get + // an access token. However, it's possible an API request doesn't need to + // be authenticated, e.g. when downloading a file from a public bucket. We + // consider this error a warning, and allow the request to go through + // without authorization, relying on the upstream API to return an error + // the user would find more helpful, should one occur. + if (err && err.message.indexOf('Could not load') === -1) { if (stream) { stream.destroy(err); } else { @@ -490,11 +395,11 @@ function makeAuthorizedRequestFactory(config) { } if (reqConfig.customEndpoint) { - // Using a custom API override. Do not use `google-auth-library` for + // Using a custom API override. Do not use `google-auto-auth` for // authentication. (ex: connecting to a local Datastore server) onAuthorized(null, reqOpts); } else { - util.authorizeRequest(reqConfig, reqOpts, onAuthorized); + authClient.authorizeRequest(reqOpts, onAuthorized); } if (stream) { @@ -502,26 +407,8 @@ function makeAuthorizedRequestFactory(config) { } } - makeAuthorizedRequest.getCredentials = function(callback) { - util.getAuthClient(config, function(err, authClient) { - if (err) { - callback(err); - return; - } - - authClient.authorize(function(err) { - if (err) { - callback(err); - return; - } - - callback(null, { - client_email: authClient.email, - private_key: authClient.key - }); - }); - }); - }; + makeAuthorizedRequest.getCredentials = + authClient.getCredentials.bind(authClient); return makeAuthorizedRequest; } diff --git a/package.json b/package.json index b0f8f8c3806..e5b4c7cfc55 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "duplexify": "^3.2.0", "extend": "^2.0.0", "gce-images": "^0.1.0", - "google-auth-library": "^0.9.4", + "google-auto-auth": "^0.2.0", "is": "^3.0.1", "methmeth": "^1.0.0", "mime-types": "^2.0.8", diff --git a/test/common/util.js b/test/common/util.js index d89868976f4..d58dcfb69c8 100644 --- a/test/common/util.js +++ b/test/common/util.js @@ -21,7 +21,7 @@ var assert = require('assert'); var duplexify = require('duplexify'); var extend = require('extend'); -var googleAuthLibrary = require('google-auth-library'); +var googleAuth = require('google-auto-auth'); var mockery = require('mockery'); var request = require('request'); var retryRequest = require('retry-request'); @@ -29,10 +29,9 @@ var stream = require('stream'); var streamForward = require('stream-forward'); var through = require('through2'); -var googleAuthLibraryOverride; -function fakeGoogleAuthLibrary() { - return (googleAuthLibraryOverride || googleAuthLibrary) - .apply(null, arguments); +var googleAutoAuthOverride; +function fakeGoogleAutoAuth() { + return (googleAutoAuthOverride || googleAuth).apply(null, arguments); } var REQUEST_DEFAULT_CONF; @@ -62,7 +61,7 @@ describe('common/util', function() { var utilOverrides = {}; before(function() { - mockery.registerMock('google-auth-library', fakeGoogleAuthLibrary); + mockery.registerMock('google-auto-auth', fakeGoogleAutoAuth); mockery.registerMock('request', fakeRequest); mockery.registerMock('retry-request', fakeRetryRequest); mockery.registerMock('stream-forward', fakeStreamForward); @@ -93,7 +92,7 @@ describe('common/util', function() { }); beforeEach(function() { - googleAuthLibraryOverride = null; + googleAutoAuthOverride = null; requestOverride = null; retryRequestOverride = null; streamForwardOverride = null; @@ -476,253 +475,42 @@ describe('common/util', function() { }); }); - describe('getAuthClient', function() { - it('should use any authClient provided as a config', function(done) { - var config = { - authClient: {} - }; - - util.getAuthClient(config, function(err, authClient) { - assert.strictEqual(authClient, config.authClient); - done(); - }); - }); - - it('should use google-auth-library', function() { - var googleAuthLibraryCalled = false; - - googleAuthLibraryOverride = function() { - googleAuthLibraryCalled = true; - return { - getApplicationDefault: util.noop - }; - }; - - util.getAuthClient({}); - - assert.strictEqual(googleAuthLibraryCalled, true); - }); - - it('should create a JWT auth client from a keyFile', function(done) { - var jwt = {}; - - googleAuthLibraryOverride = function() { - return { - JWT: function() { return jwt; } - }; - }; - - var config = { - keyFile: 'key.json', - email: 'example@example.com', - scopes: ['dev.scope'] - }; - - util.getAuthClient(config, function(err, authClient) { - assert.ifError(err); - - assert.equal(jwt.keyFile, config.keyFile); - assert.equal(jwt.email, config.email); - assert.deepEqual(jwt.scopes, config.scopes); - - assert.deepEqual(authClient, jwt); - - done(); - }); - }); - - it('should create an auth client from credentials', function(done) { - var credentialsSet; - - googleAuthLibraryOverride = function() { - return { - fromJSON: function(credentials, callback) { - credentialsSet = credentials; - callback(null, {}); - } - }; - }; - - var config = { - credentials: { a: 'b', c: 'd' } - }; - - util.getAuthClient(config, function() { - assert.deepEqual(credentialsSet, config.credentials); - done(); - }); - }); - - it('should create an auth client from magic', function(done) { - googleAuthLibraryOverride = function() { - return { - getApplicationDefault: function(callback) { - callback(null, {}); - } - }; - }; - - util.getAuthClient({}, done); - }); - - it('should scope an auth client if necessary', function(done) { - var config = { - scopes: ['a.scope', 'b.scope'] - }; - - var fakeAuthClient = { - createScopedRequired: function() { - return true; - }, - createScoped: function(scopes) { - assert.deepEqual(scopes, config.scopes); - return fakeAuthClient; - }, - getAccessToken: function() {} - }; - - googleAuthLibraryOverride = function() { - return { - getApplicationDefault: function(callback) { - callback(null, fakeAuthClient); - } - }; - }; - - util.getAuthClient(config, done); - }); - - it('should pass back any errors from the authClient', function(done) { - var error = new Error('Error!'); - - googleAuthLibraryOverride = function() { - return { - getApplicationDefault: function(callback) { - callback(error); - } - }; - }; - - util.getAuthClient({}, function(err) { - assert.strictEqual(error, err); - done(); - }); - }); - }); - - describe('authorizeRequest', function() { - it('should get an auth client', function(done) { - var config = { a: 'b', c: 'd' }; - - utilOverrides.getAuthClient = function(cfg) { - assert.deepEqual(cfg, config); - done(); - }; - - util.authorizeRequest(config); - }); - - it('should ignore "Could not load" error from google-auth', function(done) { - var reqOpts = { a: 'b', c: 'd' }; - var couldNotLoadError = new Error('Could not load'); + describe('makeAuthorizedRequestFactory', function() { + var authClient = { getCredentials: function() {} }; - utilOverrides.getAuthClient = function(config, callback) { - callback(couldNotLoadError); + beforeEach(function() { + googleAutoAuthOverride = function() { + return authClient; }; - - util.authorizeRequest({}, reqOpts, function(err, authorizedReqOpts) { - assert.ifError(err); - assert.deepEqual(reqOpts, authorizedReqOpts); - done(); - }); }); - it('should return an error to the callback', function(done) { - var error = new Error('Error.'); + it('should create an authClient', function(done) { + var config = {}; - utilOverrides.getAuthClient = function(config, callback) { - callback(error); + googleAutoAuthOverride = function(config_) { + assert.strictEqual(config_, config); + setImmediate(done); + return authClient; }; - util.authorizeRequest({}, {}, function(err) { - assert.deepEqual(err, error); - done(); - }); + util.makeAuthorizedRequestFactory(config); }); - it('should get an access token', function(done) { - var fakeAuthClient = { - getAccessToken: function() { - done(); - } - }; - - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - util.authorizeRequest(); + it('should return a function', function() { + assert.equal(typeof util.makeAuthorizedRequestFactory(), 'function'); }); - it('should return an access token error to callback', function(done) { - var error = new Error('Error.'); - - var fakeAuthClient = { - getAccessToken: function(callback) { - callback(error); - } - }; - - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - util.authorizeRequest({}, {}, function(err) { - assert.deepEqual(err, error); + it('should return a getCredentials method', function(done) { + function getCredentials() { done(); - }); - }); - - it('should extend the request options with token', function(done) { - var token = 'abctoken'; - - var reqOpts = { - uri: 'a', - headers: { - a: 'b', - c: 'd' - } - }; - - var expectedAuthorizedReqOpts = extend(true, {}, reqOpts, { - headers: { - Authorization: 'Bearer ' + token - } - }); - - var fakeAuthClient = { - getAccessToken: function(callback) { - callback(null, token); - } - }; + } - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); + googleAutoAuthOverride = function() { + return { getCredentials: getCredentials }; }; - util.authorizeRequest({}, reqOpts, function(err, authorizedReqOpts) { - assert.ifError(err); - - assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts); - - done(); - }); - }); - }); - - describe('makeAuthorizedRequestFactory', function() { - it('should return a function', function() { - assert.equal(typeof util.makeAuthorizedRequestFactory(), 'function'); + var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(); + makeAuthorizedRequest.getCredentials(); }); describe('customEndpoint (no authorization attempted)', function() { @@ -771,51 +559,55 @@ describe('common/util', function() { describe('needs authorization', function() { it('should pass correct arguments to authorizeRequest', function(done) { - var config = { a: 'b', c: 'd' }; var reqOpts = { e: 'f', g: 'h' }; - utilOverrides.authorizeRequest = function(cfg, rOpts) { - assert.deepEqual(cfg, config); + authClient.authorizeRequest = function(rOpts) { assert.deepEqual(rOpts, reqOpts); done(); }; - var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(config); + var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(); makeAuthorizedRequest(reqOpts, {}); }); it('should return a stream if callback is missing', function() { - utilOverrides.authorizeRequest = function() {}; + authClient.authorizeRequest = function() {}; var makeAuthorizedRequest = util.makeAuthorizedRequestFactory({}); assert(makeAuthorizedRequest({}) instanceof stream.Stream); }); - it('should provide stream to authorizeRequest', function(done) { - var stream; - - utilOverrides.authorizeRequest = function(cfg) { - setImmediate(function() { - assert.strictEqual(cfg.stream, stream); - done(); - }); - }; - - var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(); - stream = makeAuthorizedRequest(); - }); - describe('authorization errors', function() { var error = new Error('Error.'); beforeEach(function() { - utilOverrides.authorizeRequest = function(cfg, rOpts, callback) { + authClient.authorizeRequest = function(rOpts, callback) { setImmediate(function() { callback(error); }); }; }); + it('should not care about "Could not load" errors', function(done) { + var error = new Error('Could not load'); + + utilOverrides.decorateRequest = function() {}; + + authClient.authorizeRequest = function(rOpts, callback) { + setImmediate(function() { + callback(error); + }); + }; + + var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(); + makeAuthorizedRequest({}, { + onAuthorized: function(err) { + assert.strictEqual(err, null); + done(); + } + }); + }); + it('should invoke the callback with error', function(done) { var makeAuthorizedRequest = util.makeAuthorizedRequestFactory(); makeAuthorizedRequest({}, function(err) { @@ -852,7 +644,7 @@ describe('common/util', function() { var reqOpts = { a: 'b', c: 'd' }; beforeEach(function() { - utilOverrides.authorizeRequest = function(cfg, rOpts, callback) { + authClient.authorizeRequest = function(rOpts, callback) { callback(null, rOpts); }; }); @@ -905,113 +697,6 @@ describe('common/util', function() { }); }); }); - - describe('getCredentials', function() { - var fakeAuthClient = { - email: 'fake-email@example.com', - key: 'fake-key', - - authorize: function(callback) { callback(); } - }; - var config = { a: 'b', c: 'd' }; - - it('should return getCredentials method', function() { - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - assert.equal(typeof makeAuthorizedRequest.getCredentials, 'function'); - }); - - it('should pass config to getAuthClient', function(done) { - utilOverrides.getAuthClient = function(cfg) { - assert.deepEqual(cfg, config); - done(); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - makeAuthorizedRequest.getCredentials(); - }); - - it('should execute callback with error', function(done) { - var error = new Error('Error.'); - - utilOverrides.getAuthClient = function(config, callback) { - callback(error); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - makeAuthorizedRequest.getCredentials(function(err) { - assert.deepEqual(err, error); - done(); - }); - }); - - it('should authorize the connection', function(done) { - fakeAuthClient.authorize = function(callback) { - callback(); - }; - - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - makeAuthorizedRequest.getCredentials(done); - }); - - - it('should execute callback with authorization error', function(done) { - var error = new Error('Error.'); - - fakeAuthClient.authorize = function(cb) { - cb(error); - }; - - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - makeAuthorizedRequest.getCredentials(function(err) { - assert.deepEqual(err, error); - done(); - }); - }); - - it('should exec callback with client_email & client_key', function(done) { - fakeAuthClient.authorize = function(callback) { - callback(); - }; - - utilOverrides.getAuthClient = function(config, callback) { - callback(null, fakeAuthClient); - }; - - var makeAuthorizedRequest = - util.makeAuthorizedRequestFactory(config, assert.ifError); - - makeAuthorizedRequest.getCredentials(function(err, credentials) { - assert.deepEqual(credentials, { - client_email: fakeAuthClient.email, - private_key: fakeAuthClient.key - }); - - done(); - }); - }); - }); }); describe('shouldRetryRequest', function() {