├── .circleci ├── config.yml ├── key.json.enc ├── key.p12.enc └── key.pem.enc ├── .gitignore ├── .jshintrc ├── index.js ├── license ├── package.json ├── readme.md ├── renovate.json ├── test.js ├── test.keyfile ├── test.keyfile.json └── test.keyfile.pem /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | install_and_test: &install_and_test 4 | steps: 5 | - checkout 6 | - run: 7 | name: Decrypt JSON key 8 | command: openssl aes-256-cbc -d -in .circleci/key.json.enc -out .circleci/key.json -k "${KEY}" 9 | - run: 10 | name: Decrypt P12 key 11 | command: openssl aes-256-cbc -d -in .circleci/key.p12.enc -out .circleci/key.p12 -k "${KEY_2}" 12 | - run: 13 | name: Decrypt PEM key 14 | command: openssl aes-256-cbc -d -in .circleci/key.pem.enc -out .circleci/key.pem -k "${KEY_2}" 15 | - run: 16 | name: Install 17 | command: npm install 18 | - run: 19 | name: Test 20 | command: npm test 21 | environment: 22 | GOOGLE_APPLICATION_CREDENTIALS: /root/project/.circleci/key.json 23 | P12_EMAIL: stephen-windows@nth-circlet-705.iam.gserviceaccount.com 24 | P12_KEYFILE: /root/project/.circleci/key.p12 25 | PEM_KEYFILE: /root/project/.circleci/key.pem 26 | - run: 27 | name: Remove key 28 | command: rm .circleci/key* 29 | when: always 30 | 31 | jobs: 32 | test_node4: 33 | docker: 34 | - image: node:4 35 | <<: *install_and_test 36 | test_node6: 37 | docker: 38 | - image: node:6 39 | <<: *install_and_test 40 | test_node8: 41 | docker: 42 | - image: node:8 43 | <<: *install_and_test 44 | 45 | workflows: 46 | version: 2 47 | test: 48 | jobs: 49 | - test_node4 50 | - test_node6 51 | - test_node8 52 | -------------------------------------------------------------------------------- /.circleci/key.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/google-auto-auth/1ab3e9093c1b87200160f97db185e6cdb4046cd8/.circleci/key.json.enc -------------------------------------------------------------------------------- /.circleci/key.p12.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/google-auto-auth/1ab3e9093c1b87200160f97db185e6cdb4046cd8/.circleci/key.p12.enc -------------------------------------------------------------------------------- /.circleci/key.pem.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/google-auto-auth/1ab3e9093c1b87200160f97db185e6cdb4046cd8/.circleci/key.pem.enc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.json 4 | *.log 5 | *.p12 6 | *.pem -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "esnext": true, 6 | "freeze": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": "nofunc", 10 | "newcap": true, 11 | "node": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "strict": true, 15 | "trailing": true, 16 | "undef": true, 17 | "unused": true, 18 | "globals": { 19 | "describe": true, 20 | "it": true, 21 | "before": true, 22 | "after": true, 23 | "beforeEach": true, 24 | "afterEach": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var crypto = require('crypto'); 5 | var fs = require('fs'); 6 | var GoogleAuth = require('google-auth-library').GoogleAuth; 7 | var gcpMetadata = require('gcp-metadata'); 8 | var path = require('path'); 9 | var request = require('request'); 10 | 11 | class Auth { 12 | constructor(config) { 13 | this.authClientPromise = null; 14 | this.authClient = null; 15 | this.googleAuthClient = null; 16 | this.config = config || {}; 17 | this.credentials = null; 18 | this.environment = {}; 19 | this.jwtClient = null; 20 | this.projectId = this.config.projectId; 21 | this.token = this.config.token; 22 | } 23 | 24 | authorizeRequest (reqOpts, callback) { 25 | this.getToken((err, token) => { 26 | if (err) { 27 | callback(err); 28 | return; 29 | } 30 | 31 | var authorizedReqOpts = Object.assign({}, reqOpts, { 32 | headers: Object.assign({}, reqOpts.headers, { 33 | Authorization: `Bearer ${token}` 34 | }) 35 | }); 36 | 37 | callback(null, authorizedReqOpts); 38 | }); 39 | } 40 | 41 | getAuthClient (callback) { 42 | if (this.authClient) { 43 | // This code works around an issue with context loss with async-listener. 44 | // Strictly speaking, this should not be necessary as the call to 45 | // authClientPromise.then(..) below would resolve to the same value. 46 | // However, async-listener defaults to resuming the `then` callbacks with 47 | // the context at the point of resolution rather than the context from the 48 | // point where the `then` callback was added. In this case, the promise 49 | // will be resolved on the very first incoming http request, and that 50 | // context will become sticky (will be restored by async-listener) around 51 | // the `then` callbacks for all subsequent requests. 52 | // 53 | // This breaks APM tools like Stackdriver Trace & others and tools like 54 | // long stack traces (they will provide an incorrect stack trace). 55 | // 56 | // NOTE: this doesn't solve the problem generally. Any request concurrent 57 | // to the first call to this function, before the promise resolves, will 58 | // still lose context. We don't have a better solution at the moment :(. 59 | return setImmediate(callback.bind(null, null, this.authClient)); 60 | } 61 | 62 | var createAuthClientPromise = (resolve, reject) => { 63 | var config = this.config; 64 | var keyFile = config.keyFilename || config.keyFile; 65 | 66 | this.googleAuthClient = new GoogleAuth(); 67 | 68 | var addScope = (err, authClient, projectId) => { 69 | if (err) { 70 | reject(err); 71 | return; 72 | } 73 | 74 | if (authClient.createScopedRequired && authClient.createScopedRequired()) { 75 | if (!config.scopes || config.scopes.length === 0) { 76 | var scopeError = new Error('Scopes are required for this request.'); 77 | scopeError.code = 'MISSING_SCOPE'; 78 | reject(scopeError); 79 | return; 80 | } 81 | } 82 | 83 | authClient.scopes = config.scopes; 84 | this.authClient = authClient; 85 | this.projectId = config.projectId || projectId || authClient.projectId; 86 | 87 | if (!this.projectId) { 88 | this.googleAuthClient.getDefaultProjectId((err, projectId) => { 89 | // Ignore error, since the user might not require a project ID. 90 | 91 | if (projectId) { 92 | this.projectId = projectId; 93 | } 94 | 95 | resolve(authClient); 96 | }); 97 | return; 98 | } 99 | 100 | resolve(authClient); 101 | }; 102 | 103 | if (config.credentials) { 104 | try { 105 | var client = this.googleAuthClient.fromJSON(config.credentials); 106 | addScope(null, client); 107 | } catch (e) { 108 | addScope(e); 109 | } 110 | } else if (keyFile) { 111 | keyFile = path.resolve(process.cwd(), keyFile); 112 | 113 | fs.readFile(keyFile, (err, contents) => { 114 | if (err) { 115 | reject(err); 116 | return; 117 | } 118 | 119 | try { 120 | var client = this.googleAuthClient.fromJSON(JSON.parse(contents)); 121 | addScope(null, client); 122 | } catch(e) { 123 | // @TODO Find a better way to do this. 124 | // Ref: https://github.com/googleapis/nodejs-storage/issues/147 125 | // Ref: https://github.com/google/google-auth-library-nodejs/issues/313 126 | var client = this.googleAuthClient.fromJSON({ 127 | type: 'jwt-pem-p12', 128 | client_email: config.email, 129 | private_key: keyFile 130 | }); 131 | delete client.key; 132 | client.keyFile = keyFile; 133 | this.jwtClient = client; 134 | addScope(null, client); 135 | } 136 | }); 137 | } else { 138 | this.googleAuthClient.getApplicationDefault(addScope); 139 | } 140 | }; 141 | 142 | if (!this.authClientPromise) { 143 | this.authClientPromise = new Promise(createAuthClientPromise); 144 | } 145 | 146 | this.authClientPromise.then((authClient) => { 147 | callback(null, authClient); 148 | // The return null is needed to avoid a spurious warning if the user is 149 | // using bluebird. 150 | // See: https://github.com/stephenplusplus/google-auto-auth/issues/28 151 | return null; 152 | }).catch(callback); 153 | } 154 | 155 | getCredentials (callback) { 156 | if (this.credentials) { 157 | setImmediate(() => { 158 | callback(null, this.credentials); 159 | }); 160 | return; 161 | } 162 | 163 | this.getAuthClient((err) => { 164 | if (err) { 165 | callback(err); 166 | return; 167 | } 168 | 169 | this.googleAuthClient.getCredentials((err, credentials) => { 170 | if (err) { 171 | callback(err); 172 | return; 173 | } 174 | 175 | this.credentials = credentials; 176 | 177 | if (this.jwtClient) { 178 | this.jwtClient.authorize(err => { 179 | if (err) { 180 | callback(err); 181 | return; 182 | } 183 | 184 | this.credentials.private_key = this.jwtClient.key; 185 | 186 | callback(null, this.credentials); 187 | }); 188 | return; 189 | } 190 | 191 | callback(null, this.credentials); 192 | }); 193 | }); 194 | } 195 | 196 | getEnvironment (callback) { 197 | async.parallel([ 198 | cb => this.isAppEngine(cb), 199 | cb => this.isCloudFunction(cb), 200 | cb => this.isComputeEngine(cb), 201 | cb => this.isContainerEngine(cb) 202 | ], () => { 203 | callback(null, this.environment); 204 | }); 205 | } 206 | 207 | getProjectId (callback) { 208 | if (this.projectId) { 209 | setImmediate(() => { 210 | callback(null, this.projectId); 211 | }); 212 | return; 213 | } 214 | 215 | this.getAuthClient(err => { 216 | if (err) { 217 | callback(err); 218 | return; 219 | } 220 | 221 | callback(null, this.projectId); 222 | }); 223 | } 224 | 225 | getToken (callback) { 226 | if (this.token) { 227 | setImmediate(callback, null, this.token); 228 | return; 229 | } 230 | 231 | this.getAuthClient((err, client) => { 232 | if (err) { 233 | callback(err); 234 | return; 235 | } 236 | 237 | client.getAccessToken(callback); 238 | }); 239 | } 240 | 241 | isAppEngine (callback) { 242 | setImmediate(() => { 243 | var env = this.environment; 244 | 245 | if (typeof env.IS_APP_ENGINE === 'undefined') { 246 | env.IS_APP_ENGINE = !!(process.env.GAE_SERVICE || process.env.GAE_MODULE_NAME); 247 | } 248 | 249 | callback(null, env.IS_APP_ENGINE); 250 | }); 251 | } 252 | 253 | isCloudFunction (callback) { 254 | setImmediate(() => { 255 | var env = this.environment; 256 | 257 | if (typeof env.IS_CLOUD_FUNCTION === 'undefined') { 258 | env.IS_CLOUD_FUNCTION = !!process.env.FUNCTION_NAME; 259 | } 260 | 261 | callback(null, env.IS_CLOUD_FUNCTION); 262 | }); 263 | } 264 | 265 | isComputeEngine (callback) { 266 | var env = this.environment; 267 | 268 | if (typeof env.IS_COMPUTE_ENGINE !== 'undefined') { 269 | setImmediate(() => { 270 | callback(null, env.IS_COMPUTE_ENGINE); 271 | }); 272 | return; 273 | } 274 | 275 | request('http://metadata.google.internal', (err, res) => { 276 | env.IS_COMPUTE_ENGINE = !err && res.headers['metadata-flavor'] === 'Google'; 277 | 278 | callback(null, env.IS_COMPUTE_ENGINE); 279 | }); 280 | } 281 | 282 | isContainerEngine (callback) { 283 | var env = this.environment; 284 | 285 | if (typeof env.IS_CONTAINER_ENGINE !== 'undefined') { 286 | setImmediate(() => { 287 | callback(null, env.IS_CONTAINER_ENGINE); 288 | }); 289 | return; 290 | } 291 | 292 | gcpMetadata.instance('/attributes/cluster-name') 293 | .then(() => { 294 | env.IS_CONTAINER_ENGINE = true; 295 | callback(null, env.IS_CONTAINER_ENGINE); 296 | }) 297 | .catch(() => { 298 | env.IS_CONTAINER_ENGINE = false 299 | callback(null, env.IS_CONTAINER_ENGINE); 300 | }); 301 | } 302 | 303 | sign (data, callback) { 304 | this.getCredentials((err, credentials) => { 305 | if (err) { 306 | callback(err); 307 | return; 308 | } 309 | 310 | if (credentials.private_key) { 311 | this._signWithPrivateKey(data, callback); 312 | } else { 313 | this._signWithApi(data, callback); 314 | } 315 | }); 316 | } 317 | 318 | // `this.getCredentials()` will always have been run by this time 319 | _signWithApi (data, callback) { 320 | if (!this.projectId) { 321 | callback(new Error('Cannot sign data without a project ID.')); 322 | return; 323 | } 324 | 325 | var client_email = this.credentials.client_email; 326 | 327 | if (!client_email) { 328 | callback(new Error('Cannot sign data without `client_email`.')); 329 | return; 330 | } 331 | 332 | var idString = `projects/${this.projectId}/serviceAccounts/${client_email}`; 333 | 334 | var reqOpts = { 335 | method: 'POST', 336 | uri: `https://iam.googleapis.com/v1/${idString}:signBlob`, 337 | json: { 338 | bytesToSign: Buffer.from(data).toString('base64') 339 | } 340 | }; 341 | 342 | this.authorizeRequest(reqOpts, (err, authorizedReqOpts) => { 343 | if (err) { 344 | callback(err); 345 | return; 346 | } 347 | 348 | request(authorizedReqOpts, function (err, resp, body) { 349 | var response = resp.toJSON(); 350 | 351 | if (!err && response.statusCode < 200 || response.statusCode >= 400) { 352 | if (typeof response.body === 'object') { 353 | var apiError = response.body.error; 354 | err = new Error(apiError.message); 355 | Object.assign(err, apiError); 356 | } else { 357 | err = new Error(response.body); 358 | err.code = response.statusCode; 359 | } 360 | } 361 | 362 | callback(err, body && body.signature); 363 | }); 364 | }); 365 | } 366 | 367 | // `this.getCredentials()` will always have been run by this time 368 | _signWithPrivateKey (data, callback) { 369 | var sign = crypto.createSign('RSA-SHA256'); 370 | sign.update(data); 371 | callback(null, sign.sign(this.credentials.private_key, 'base64')); 372 | } 373 | } 374 | 375 | module.exports = config => { 376 | return new Auth(config); 377 | }; 378 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stephen Sawchuk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-auto-auth", 3 | "version": "0.10.1", 4 | "description": "Making it as easy as possible to authenticate a Google API request", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "test": "mocha --timeout 0" 11 | }, 12 | "keywords": [ 13 | "google", 14 | "authentication", 15 | "jwt", 16 | "service", 17 | "account", 18 | "googleapis", 19 | "gcloud", 20 | "cloud", 21 | "gce", 22 | "compute", 23 | "engine", 24 | "auth", 25 | "access", 26 | "token" 27 | ], 28 | "author": "Stephen Sawchuk ", 29 | "repository": "stephenplusplus/google-auto-auth", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "mocha": "^5.0.0", 33 | "mockery": "^2.0.0" 34 | }, 35 | "dependencies": { 36 | "async": "^2.3.0", 37 | "gcp-metadata": "^0.7.0", 38 | "google-auth-library": "^1.3.1", 39 | "request": "^2.79.0" 40 | }, 41 | "engines": { 42 | "node": ">=4.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # google-auto-auth 2 | > Making it as easy as possible to authenticate a Google API request 3 | 4 | ```sh 5 | $ npm install --save google-auto-auth 6 | ``` 7 | ```js 8 | var googleAuth = require('google-auto-auth'); 9 | 10 | // Create a client 11 | var auth = googleAuth(); 12 | 13 | auth.authorizeRequest({ 14 | method: 'get', 15 | uri: 'https://www.googleapis.com/something' 16 | }, function (err, authorizedReqOpts) { 17 | /* 18 | authorizedReqOpts = { 19 | method: 'get', 20 | uri: 'https://www.googleapis.com/something', 21 | headers: { 22 | Authorization: 'Bearer {{token}}' 23 | } 24 | } 25 | */ 26 | }); 27 | ``` 28 | 29 | Or, just get an access token. 30 | ```js 31 | auth.getToken(function (err, token) { 32 | /* 33 | token = 'access token' 34 | */ 35 | }); 36 | ``` 37 | 38 | 39 | This works automatically **if**: 40 | 41 | - your app runs on Google Cloud Platform 42 | - you are authenticated with the `gcloud` sdk 43 | - you have the path to a JSON key file as an environment variable named `GOOGLE_APPLICATION_CREDENTIALS` 44 | 45 | If you do not meet those, you must provide a `keyFilename` or `credentials` object. 46 | 47 | ```js 48 | var googleAuth = require('google-auto-auth'); 49 | 50 | var authConfig = {}; 51 | 52 | // path to a key: 53 | authConfig.keyFilename = '/path/to/keyfile.json'; 54 | 55 | // or a credentials object: 56 | authConfig.credentials = { 57 | client_email: '...', 58 | private_key: '...' 59 | }; 60 | 61 | // Create a client 62 | var auth = googleAuth(authConfig); 63 | 64 | auth.authorizeRequest({/*...*/}, function (err, authorizedReqOpts) {}); 65 | auth.getToken(function (err, token) {}); 66 | ``` 67 | 68 | ### API 69 | 70 | #### googleAuth = require('google-auto-auth') 71 | 72 | #### auth = googleAuth([authConfig]) 73 | 74 | ##### authConfig 75 | 76 | - Type: `Object` 77 | 78 | See the above section on Authentication. This object is necessary if automatic authentication is not available in your environment. 79 | 80 | At a glance, the supported properties for this method are: 81 | 82 | - `credentials` - Object containing `client_email` and `private_key` properties 83 | - `keyFilename` - Path to a .json, .pem, or .p12 key file 84 | - `projectId` - Your project ID 85 | - `scopes` - Required scopes for the desired API request 86 | - `token` - An access token. If provided, we'll use this instead of fetching a new one 87 | 88 | #### auth.authorizeRequest(reqOpts, callback) 89 | 90 | Extend an HTTP request object with an authorized header. 91 | 92 | ##### callback(err, authorizedReqOpts) 93 | 94 | ###### callback.err 95 | 96 | - Type: `Error` 97 | 98 | An API error or an error if scopes are required for the request you're trying to make (check for err.code = `MISSING_SCOPE`). If you receive the missing scope error, provide the `authConfig.scopes` array with the necessary scope URLs for your request. There are examples of scopes that are required for some of the Google Cloud Platform services in the [gcloud-node Authentication Guide](https://googlecloudplatform.github.io/gcloud-node/#/authentication). 99 | 100 | ###### callback.authorizedReqOpts 101 | 102 | - Type: `Object` 103 | 104 | The reqOpts object provided has been extended with a valid access token attached to the `headers.Authorization` value. E.g.: `headers.Authorization = 'Bearer y.2343...'`. 105 | 106 | #### auth.getAuthClient(callback) 107 | 108 | Get the auth client instance from [google-auth-library](http://gitnpm.com/googleauth). 109 | 110 | ##### callback(err, authClient) 111 | 112 | ###### callback.err 113 | 114 | - Type: `Error` 115 | 116 | An error that occurred while trying to get an authorization client. 117 | 118 | ###### callback.authClient 119 | 120 | - Type: [`google-auth-library`](http://gitnpm.com/googleauth) 121 | 122 | The client instance from [google-auth-library](http://gitnpm.com/googleauth). This is the underlying object this library uses. 123 | 124 | 125 | #### auth.getCredentials(callback) 126 | 127 | Get the `client_email` and `private_key` properties from an authorized client. 128 | 129 | ##### callback(err, credentials) 130 | 131 | ###### callback.err 132 | 133 | - Type: `Error` 134 | 135 | An error that occurred while trying to get an authorization client. 136 | 137 | ###### callback.credentials 138 | 139 | - Type: `Object` 140 | 141 | An object containing `client_email` and `private_key`. 142 | 143 | 144 | #### auth.getEnvironment(callback) 145 | 146 | Determine if the environment the app is running in is a Google Compute Engine instance. 147 | 148 | ##### callback(err, environmentObject) 149 | 150 | ###### callback.err 151 | 152 | - Type: `Null` 153 | 154 | We won't return an error, but it's here for convention-sake. 155 | 156 | ###### callback.environmentObject 157 | 158 | - Type: `Object` 159 | 160 | ```js 161 | { 162 | IS_APP_ENGINE: Boolean, 163 | IS_CLOUD_FUNCTION: Boolean, 164 | IS_COMPUTE_ENGINE: Boolean, 165 | IS_CONTAINER_ENGINE: Boolean 166 | } 167 | ``` 168 | 169 | If you've already run this function, the object will persist as `auth.environment`. 170 | 171 | 172 | #### auth.getProjectId(callback) 173 | 174 | Get the project ID if it was auto-detected or parsed from the provided keyfile. 175 | 176 | ##### callback(err, projectId) 177 | 178 | ###### callback.err 179 | 180 | - Type: `Error` 181 | 182 | An error that occurred while trying to get an authorization client. 183 | 184 | ###### callback.projectId 185 | 186 | - Type: `string` 187 | 188 | The project ID that was parsed from the provided key file or auto-detected from the environment. 189 | 190 | 191 | #### auth.getToken(callback) 192 | 193 | Get an access token. The token will always be current. If necessary, background refreshes are handled automatically. 194 | 195 | ##### callback(err, token) 196 | 197 | ###### callback.err 198 | 199 | - Type: `Error` 200 | 201 | An API error or an error if scopes are required for the request you're trying to make (check for err.code = `MISSING_SCOPE`). If you receive the missing scope error, provide the `authConfig.scopes` array with the necessary scope URLs for your request. 202 | 203 | ###### callback.token 204 | 205 | - Type: `String` 206 | 207 | A current access token to be used during an API request. If you provided `authConfig.token`, this method simply returns the value you passed. 208 | 209 | 210 | #### auth.isAppEngine(callback) 211 | 212 | Determine if the environment the app is running in is a Google App Engine instance. 213 | 214 | ##### callback(err, isAppEngine) 215 | 216 | ###### callback.err 217 | 218 | - Type: `Null` 219 | 220 | We won't return an error, but it's here for convention-sake. 221 | 222 | ###### callback.isAppEngine 223 | 224 | - Type: `Boolean` 225 | 226 | Whether the app is in App Engine or not. 227 | 228 | 229 | #### auth.isCloudFunction(callback) 230 | 231 | Determine if the environment the app is running in is a Google Cloud Function. 232 | 233 | ##### callback(err, isCloudFunction) 234 | 235 | ###### callback.err 236 | 237 | - Type: `Null` 238 | 239 | We won't return an error, but it's here for convention-sake. 240 | 241 | ###### callback.isCloudFunction 242 | 243 | - Type: `Boolean` 244 | 245 | Whether the app is in a Cloud Function or not. 246 | 247 | 248 | #### auth.isComputeEngine(callback) 249 | 250 | Determine if the environment the app is running in is a Google Compute Engine instance. 251 | 252 | ##### callback(err, isComputeEngine) 253 | 254 | ###### callback.err 255 | 256 | - Type: `Null` 257 | 258 | We won't return an error, but it's here for convention-sake. 259 | 260 | ###### callback.isComputeEngine 261 | 262 | - Type: `Boolean` 263 | 264 | Whether the app is in a Compute Engine instance or not. 265 | 266 | 267 | #### auth.isContainerEngine(callback) 268 | 269 | Determine if the environment the app is running in is a Google Container Engine instance. 270 | 271 | ##### callback(err, isContainerEngine) 272 | 273 | ###### callback.err 274 | 275 | - Type: `Null` 276 | 277 | We won't return an error, but it's here for convention-sake. 278 | 279 | ###### callback.isContainerEngine 280 | 281 | - Type: `Boolean` 282 | 283 | Whether the app is in a Container Engine instance or not. 284 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "pinDigests": false, 7 | "pinVersions": false, 8 | "rebaseStalePrs": true 9 | } 10 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var async = require('async'); 5 | var fs = require('fs'); 6 | var mockery = require('mockery'); 7 | var path = require('path'); 8 | 9 | var createSignOverride; 10 | var fakeCrypto = { 11 | createSign: function () { 12 | return (createSignOverride || function () {}).apply(null, arguments); 13 | } 14 | }; 15 | 16 | var GoogleAuthOverride; 17 | var fakeGoogleAuthLibrary = { 18 | GoogleAuth: function () { 19 | return (GoogleAuthOverride || function () {}).apply(null, arguments); 20 | }, 21 | }; 22 | 23 | var requestOverride; 24 | function fakeRequest() { 25 | return (requestOverride || function () {}).apply(null, arguments); 26 | } 27 | 28 | var instanceOverride; 29 | var fakeGcpMetadata = { 30 | instance: function () { 31 | return (instanceOverride || Promise.resolve).apply(null, arguments); 32 | } 33 | }; 34 | 35 | describe('googleAutoAuth', function () { 36 | var googleAutoAuth; 37 | var auth; 38 | 39 | before(function () { 40 | mockery.registerMock('google-auth-library', fakeGoogleAuthLibrary); 41 | mockery.registerMock('crypto', fakeCrypto); 42 | mockery.registerMock('request', fakeRequest); 43 | mockery.registerMock('gcp-metadata', fakeGcpMetadata); 44 | 45 | mockery.enable({ 46 | useCleanCache: true, 47 | warnOnUnregistered: false 48 | }); 49 | 50 | googleAutoAuth = require('./index.js'); 51 | }); 52 | 53 | after(function () { 54 | mockery.deregisterAll(); 55 | mockery.disable(); 56 | }); 57 | 58 | beforeEach(function () { 59 | createSignOverride = null; 60 | requestOverride = null; 61 | GoogleAuthOverride = null; 62 | instanceOverride = null; 63 | auth = googleAutoAuth(); 64 | }); 65 | 66 | describe('constructor', function () { 67 | it('should set correct defaults', function () { 68 | assert.strictEqual(auth.authClientPromise, null); 69 | assert.strictEqual(auth.authClient, null); 70 | assert.strictEqual(auth.googleAuthClient, null); 71 | assert.deepStrictEqual(auth.config, {}); 72 | assert.strictEqual(auth.credentials, null); 73 | assert.deepStrictEqual(auth.environment, {}); 74 | assert.strictEqual(auth.projectId, undefined); 75 | assert.strictEqual(auth.jwtClient, null); 76 | assert.strictEqual(auth.token, undefined); 77 | }); 78 | 79 | it('should cache config', function () { 80 | var config = {}; 81 | var auth = googleAutoAuth(config); 82 | 83 | assert.strictEqual(auth.config, config); 84 | }); 85 | 86 | it('should cache project ID', function () { 87 | var auth = googleAutoAuth({ 88 | projectId: 'project-id' 89 | }); 90 | 91 | assert.strictEqual(auth.projectId, 'project-id'); 92 | }); 93 | 94 | it('should cache token', function () { 95 | var auth = googleAutoAuth({ 96 | token: 'custom token' 97 | }); 98 | 99 | assert.strictEqual(auth.token, 'custom token'); 100 | }); 101 | }); 102 | 103 | describe('authorizeRequest', function () { 104 | it('should get a token', function (done) { 105 | auth.getToken = function () { 106 | done(); 107 | }; 108 | 109 | auth.authorizeRequest({}, assert.ifError); 110 | }); 111 | 112 | it('should execute callback with error', function (done) { 113 | var error = new Error('Error.'); 114 | 115 | auth.getToken = function (callback) { 116 | callback(error); 117 | }; 118 | 119 | auth.authorizeRequest({}, function (err) { 120 | assert.strictEqual(err, error); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should extend the request options with token', function (done) { 126 | var token = 'abctoken'; 127 | 128 | var reqOpts = { 129 | uri: 'a', 130 | headers: { 131 | a: 'b', 132 | c: 'd' 133 | } 134 | }; 135 | 136 | var expectedAuthorizedReqOpts = Object.assign({}, reqOpts); 137 | expectedAuthorizedReqOpts.headers = Object.assign({ 138 | Authorization: `Bearer ${token}` 139 | }, reqOpts.headers); 140 | 141 | auth.getToken = function (callback) { 142 | callback(null, token); 143 | }; 144 | 145 | auth.authorizeRequest(reqOpts, function (err, authorizedReqOpts) { 146 | assert.ifError(err); 147 | assert.notStrictEqual(authorizedReqOpts, reqOpts); 148 | assert.notDeepEqual(authorizedReqOpts, reqOpts); 149 | assert.deepEqual(authorizedReqOpts, expectedAuthorizedReqOpts); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | 155 | describe('getAuthClient', function () { 156 | beforeEach(function () { 157 | process.chdir(__dirname); 158 | }); 159 | 160 | it('should re-use an existing authClient', function (done) { 161 | auth.authClient = { a: 'b', c: 'd' }; 162 | 163 | auth.getAuthClient(function (err, authClient) { 164 | assert.strictEqual(authClient, auth.authClient); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should re-use an existing authClientPromise', function (done) { 170 | auth.authClientPromise = Promise.resolve(42); 171 | 172 | auth.getAuthClient(function (err, authClient) { 173 | assert.ifError(err); 174 | assert.strictEqual(authClient, 42); 175 | done(); 176 | }); 177 | }); 178 | 179 | it('should create an authClientPromise', function (done) { 180 | var authClient = { 181 | projectId: 'project-id' 182 | }; 183 | 184 | GoogleAuthOverride = function () { 185 | return { 186 | getApplicationDefault: function (callback) { 187 | callback(null, authClient); 188 | } 189 | }; 190 | }; 191 | 192 | auth.getAuthClient(function (err, _authClient) { 193 | assert.ifError(err); 194 | 195 | assert.strictEqual(_authClient, authClient); 196 | 197 | auth.authClientPromise 198 | .then(function (authClientFromPromise) { 199 | assert.strictEqual(authClientFromPromise, authClient); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | 205 | it('should use google-auth-library', function () { 206 | var googleAuthLibraryCalled = false; 207 | 208 | GoogleAuthOverride = function () { 209 | googleAuthLibraryCalled = true; 210 | return { 211 | getApplicationDefault: function () {} 212 | }; 213 | }; 214 | 215 | auth.getAuthClient(assert.ifError); 216 | assert.strictEqual(googleAuthLibraryCalled, true); 217 | }); 218 | 219 | it('should cache googleAuthClient', function () { 220 | var googleAuthClient = { 221 | getApplicationDefault: function () {} 222 | }; 223 | 224 | GoogleAuthOverride = function () { 225 | return googleAuthClient; 226 | }; 227 | 228 | auth.getAuthClient(assert.ifError); 229 | assert.strictEqual(auth.googleAuthClient, googleAuthClient); 230 | }); 231 | 232 | it('should create a google auth client from JSON', function (done) { 233 | auth.config = { 234 | keyFile: '../test.keyfile.json', 235 | scopes: ['dev.scope'] 236 | }; 237 | 238 | var expectedJson = require('./test.keyfile.json'); 239 | 240 | var googleAuthClient = { 241 | createScopedRequired: function () {}, 242 | projectId: 'project-id' 243 | }; 244 | 245 | GoogleAuthOverride = function () { 246 | return { 247 | fromJSON: function (json) { 248 | assert.deepEqual(json, expectedJson); 249 | return googleAuthClient; 250 | } 251 | }; 252 | }; 253 | 254 | // to test that `path.resolve` is being used 255 | process.chdir('node_modules'); 256 | 257 | auth.getAuthClient(function (err, authClient) { 258 | assert.ifError(err); 259 | 260 | assert.strictEqual(authClient.scopes, auth.config.scopes); 261 | assert.strictEqual(auth.authClient, googleAuthClient); 262 | assert.strictEqual(authClient, googleAuthClient); 263 | 264 | done(); 265 | }); 266 | }); 267 | 268 | it('should see if a file reads as JSON', function (done) { 269 | auth.config = { 270 | keyFile: '../test.keyfile', 271 | scopes: ['dev.scope'] 272 | }; 273 | 274 | var expectedJson = JSON.parse(fs.readFileSync('./test.keyfile')); 275 | 276 | var googleAuthClient = { 277 | createScopedRequired: function () {}, 278 | projectId: 'project-id' 279 | }; 280 | 281 | GoogleAuthOverride = function () { 282 | return { 283 | fromJSON: function (json) { 284 | assert.deepEqual(json, expectedJson); 285 | return googleAuthClient; 286 | } 287 | }; 288 | }; 289 | 290 | // to test that `path.resolve` is being used 291 | process.chdir('node_modules'); 292 | 293 | auth.getAuthClient(function (err, authClient) { 294 | assert.ifError(err); 295 | 296 | assert.strictEqual(authClient.scopes, auth.config.scopes); 297 | assert.strictEqual(auth.authClient, googleAuthClient); 298 | assert.strictEqual(authClient, googleAuthClient); 299 | 300 | done(); 301 | }); 302 | }); 303 | 304 | it('should create an auth client from credentials', function (done) { 305 | var googleAuthClient = { 306 | createScopedRequired: function () {}, 307 | projectId: 'project-id' 308 | }; 309 | 310 | GoogleAuthOverride = function () { 311 | return { 312 | fromJSON: function (json) { 313 | assert.deepEqual(json, auth.config.credentials); 314 | return googleAuthClient; 315 | } 316 | }; 317 | }; 318 | 319 | auth.config = { 320 | credentials: { a: 'b', c: 'd' } 321 | }; 322 | 323 | auth.getAuthClient(function (err, authClient) { 324 | assert.ifError(err); 325 | 326 | assert.strictEqual(auth.authClient, googleAuthClient); 327 | assert.strictEqual(authClient, googleAuthClient); 328 | 329 | done(); 330 | }); 331 | }); 332 | 333 | it('should return error if file does not exist', function (done) { 334 | GoogleAuthOverride = function () {}; 335 | 336 | auth.config = { 337 | keyFilename: 'non-existent-key.pem' 338 | }; 339 | 340 | auth.getAuthClient(function (err) { 341 | assert(err.message.includes('no such file or directory')); 342 | done(); 343 | }); 344 | }); 345 | 346 | it('should create a JWT auth client from non-JSON', function (done) { 347 | var jwt = { 348 | createScopedRequired: function () {}, 349 | projectId: 'project-id' 350 | }; 351 | 352 | auth.config = { 353 | keyFilename: './test.keyfile.pem', 354 | email: 'example@example.com', 355 | scopes: ['dev.scope'] 356 | }; 357 | 358 | var expectedKey = path.resolve(process.cwd(), auth.config.keyFilename); 359 | 360 | GoogleAuthOverride = function () { 361 | return { 362 | fromJSON: function (config) { 363 | assert.strictEqual(config.type, 'jwt-pem-p12'); 364 | assert.strictEqual(config.client_email, auth.config.email); 365 | 366 | assert.strictEqual(config.private_key, expectedKey); 367 | 368 | return jwt; 369 | } 370 | }; 371 | }; 372 | 373 | auth.getAuthClient(function (err, authClient) { 374 | assert.ifError(err); 375 | assert.strictEqual(auth.authClient, jwt); 376 | assert.strictEqual(auth.jwtClient, jwt); 377 | 378 | assert.strictEqual(jwt.key, undefined); 379 | assert.strictEqual(jwt.keyFile, expectedKey); 380 | 381 | assert.strictEqual(authClient, jwt); 382 | done(); 383 | }); 384 | }); 385 | 386 | it('should create an auth client from magic', function (done) { 387 | var googleAuthClient = { 388 | createScopedRequired: function () {}, 389 | projectId: 'project-id' 390 | }; 391 | 392 | GoogleAuthOverride = function () { 393 | return { 394 | getApplicationDefault: function (callback) { 395 | callback(null, googleAuthClient); 396 | } 397 | }; 398 | }; 399 | 400 | auth.getAuthClient(function (err, authClient) { 401 | assert.ifError(err); 402 | 403 | assert.strictEqual(auth.authClient, googleAuthClient); 404 | assert.strictEqual(authClient, googleAuthClient); 405 | 406 | done(); 407 | }); 408 | }); 409 | 410 | it('should prefer the user-provided project ID', function (done) { 411 | var googleAuthClient = { 412 | createScopedRequired: function () {}, 413 | projectId: 'project-id' 414 | }; 415 | var badProjectId = 'bad-project-id'; 416 | var goodProjectId = 'good-project-id'; 417 | 418 | GoogleAuthOverride = function () { 419 | return { 420 | getApplicationDefault: function (callback) { 421 | callback(null, googleAuthClient, badProjectId); 422 | } 423 | }; 424 | }; 425 | 426 | auth.config = { 427 | projectId: goodProjectId 428 | }; 429 | 430 | auth.getAuthClient(function (err) { 431 | assert.ifError(err); 432 | assert.strictEqual(auth.projectId, goodProjectId); 433 | done(); 434 | }); 435 | }); 436 | 437 | it('should get a project ID from auth client', function (done) { 438 | var googleAuthClient = { 439 | createScopedRequired: function () {} 440 | }; 441 | 442 | var projectId = 'detected-default-project-id'; 443 | 444 | GoogleAuthOverride = function () { 445 | return { 446 | getApplicationDefault: function (callback) { 447 | callback(null, googleAuthClient); 448 | }, 449 | getDefaultProjectId: function (callback) { 450 | callback(null, projectId); 451 | } 452 | }; 453 | }; 454 | 455 | auth.config = {}; 456 | 457 | auth.getAuthClient(function (err) { 458 | assert.ifError(err); 459 | assert.strictEqual(auth.projectId, projectId); 460 | done(); 461 | }); 462 | }); 463 | 464 | it('should return scope error if necessary', function (done) { 465 | auth.config = { 466 | scopes: [] 467 | }; 468 | 469 | var fakeAuthClient = { 470 | createScopedRequired: function () { 471 | return true; 472 | }, 473 | projectId: 'project-id' 474 | }; 475 | 476 | GoogleAuthOverride = function () { 477 | return { 478 | getApplicationDefault: function (callback) { 479 | callback(null, fakeAuthClient); 480 | } 481 | }; 482 | }; 483 | 484 | auth.getAuthClient(function (e) { 485 | assert.strictEqual(e.code, 'MISSING_SCOPE'); 486 | assert.strictEqual(e.message, 'Scopes are required for this request.'); 487 | done(); 488 | }); 489 | }); 490 | 491 | it('should pass back any errors from the authClient', function (done) { 492 | var error = new Error('Error.'); 493 | 494 | GoogleAuthOverride = function () { 495 | return { 496 | getApplicationDefault: function (callback) { 497 | callback(error); 498 | }, 499 | projectId: 'project-id' 500 | }; 501 | }; 502 | 503 | auth.getAuthClient(function (err) { 504 | assert.strictEqual(err,error); 505 | done(); 506 | }); 507 | }); 508 | }); 509 | 510 | describe('getCredentials', function () { 511 | it('should return a cached credentials object', function (done) { 512 | auth.getAuthClient = function () { 513 | throw new Error('Should not be executed.') 514 | }; 515 | 516 | auth.credentials = {}; 517 | 518 | auth.getCredentials(function (err, credentials) { 519 | assert.ifError(err); 520 | assert.strictEqual(credentials, auth.credentials); 521 | done(); 522 | }) 523 | }); 524 | 525 | it('should get an auth client', function (done) { 526 | auth.getAuthClient = function () { 527 | done(); 528 | }; 529 | 530 | auth.getCredentials(assert.ifError); 531 | }); 532 | 533 | it('should execute callback with error', function (done) { 534 | var error = new Error('Error.'); 535 | 536 | auth.getAuthClient = function (callback) { 537 | callback(error); 538 | }; 539 | 540 | auth.getCredentials(function (err) { 541 | assert.strictEqual(err, error); 542 | done(); 543 | }); 544 | }); 545 | 546 | it('should execute callback with object', function (done) { 547 | var credentialsFromAuthClient = {}; 548 | 549 | auth.googleAuthClient = { 550 | getCredentials: function (callback) { 551 | callback(null, credentialsFromAuthClient); 552 | } 553 | }; 554 | 555 | auth.getAuthClient = function (callback) { 556 | callback(); 557 | }; 558 | 559 | auth.getCredentials(function (err, creds) { 560 | assert.ifError(err); 561 | 562 | assert.strictEqual(creds, credentialsFromAuthClient); 563 | assert.strictEqual(auth.credentials, credentialsFromAuthClient); 564 | 565 | done(); 566 | }); 567 | }); 568 | 569 | it('should execute callback with error from client', function (done) { 570 | var error = new Error('Error.'); 571 | 572 | auth.googleAuthClient = { 573 | getCredentials: function (callback) { 574 | callback(error); 575 | } 576 | }; 577 | 578 | auth.getAuthClient = function (callback) { 579 | callback(); 580 | }; 581 | 582 | auth.getCredentials(function (err) { 583 | assert.strictEqual(err, error); 584 | done(); 585 | }); 586 | }); 587 | 588 | describe('JWT client', function () { 589 | var CREDENTIALS = {}; 590 | var PRIVATE_KEY = 'private-key'; 591 | 592 | beforeEach(function () { 593 | CREDENTIALS = {}; 594 | 595 | auth.jwtClient = { 596 | authorize: function (callback) { 597 | auth.jwtClient.key = PRIVATE_KEY; 598 | callback(); 599 | } 600 | }; 601 | 602 | auth.googleAuthClient = { 603 | getCredentials: function (callback) { 604 | callback(null, CREDENTIALS); 605 | } 606 | }; 607 | 608 | auth.getAuthClient = function (callback) { 609 | callback(); 610 | }; 611 | }); 612 | 613 | it('should authorize the client', function (done) { 614 | auth.jwtClient.authorize = function () { 615 | done(); 616 | }; 617 | 618 | auth.getCredentials(assert.ifError); 619 | }); 620 | 621 | it('should return an error from authorization', function (done) { 622 | var error = new Error('Error.'); 623 | 624 | auth.jwtClient.authorize = function (callback) { 625 | callback(error); 626 | }; 627 | 628 | auth.getCredentials(function (err) { 629 | assert.strictEqual(err, error); 630 | done(); 631 | }); 632 | }); 633 | 634 | it('should set private_key property', function (done) { 635 | auth.getCredentials(function (err, credentials) { 636 | assert.ifError(err); 637 | assert.strictEqual(credentials.private_key, PRIVATE_KEY); 638 | done(); 639 | }); 640 | }); 641 | }); 642 | }); 643 | 644 | describe('getEnvironment', function () { 645 | it('should call all environment detection methods', function (done) { 646 | auth.isAppEngine = function (callback) { 647 | callback(); 648 | }; 649 | 650 | auth.isCloudFunction = function (callback) { 651 | callback(); 652 | }; 653 | 654 | auth.isComputeEngine = function (callback) { 655 | callback(); 656 | }; 657 | 658 | auth.isContainerEngine = function (callback) { 659 | callback(); 660 | }; 661 | 662 | auth.getEnvironment(function (err, environment) { 663 | assert.ifError(err); 664 | assert.strictEqual(environment, auth.environment); 665 | done(); 666 | }); 667 | }); 668 | }); 669 | 670 | describe('getProjectId', function () { 671 | var PROJECT_ID = 'project-id'; 672 | 673 | it('should return a project ID if already set', function (done) { 674 | auth.getAuthClient = function () { 675 | done(); // Will cause the test to blow up 676 | }; 677 | 678 | auth.projectId = PROJECT_ID; 679 | 680 | auth.getProjectId(function (err, projectId) { 681 | assert.ifError(err); 682 | assert.strictEqual(projectId, PROJECT_ID); 683 | done(); 684 | }); 685 | }); 686 | 687 | it('should get an auth client', function (done) { 688 | auth.getAuthClient = function () { 689 | done(); 690 | }; 691 | 692 | auth.getProjectId(assert.ifError); 693 | }); 694 | 695 | it('should execute callback with error', function (done) { 696 | var error = new Error('Error.'); 697 | 698 | auth.getAuthClient = function (callback) { 699 | callback(error); 700 | }; 701 | 702 | auth.getProjectId(function (err) { 703 | assert.strictEqual(err, error); 704 | done(); 705 | }); 706 | }); 707 | 708 | it('should get a project ID', function (done) { 709 | auth.getAuthClient = function (callback) { 710 | auth.projectId = PROJECT_ID; 711 | callback(); 712 | }; 713 | 714 | auth.getProjectId(function (err, projectId) { 715 | assert.ifError(err); 716 | assert.strictEqual(projectId, PROJECT_ID); 717 | done(); 718 | }); 719 | }); 720 | }); 721 | 722 | describe('getToken', function () { 723 | it('should return token if provided by user', function (done) { 724 | auth.getAuthClient = function () { 725 | throw new Error('Should not have called auth client'); 726 | }; 727 | 728 | auth.token = 'abc'; 729 | 730 | auth.getToken(function (err, token) { 731 | assert.ifError(err); 732 | assert.strictEqual(token, auth.token); 733 | done(); 734 | }); 735 | }); 736 | 737 | it('should get an auth client', function (done) { 738 | auth.getAuthClient = function () { 739 | done(); 740 | }; 741 | 742 | auth.getToken(assert.ifError); 743 | }); 744 | 745 | it('should execute callback with error', function (done) { 746 | var error = new Error('Error.'); 747 | 748 | auth.getAuthClient = function (callback) { 749 | callback(error); 750 | }; 751 | 752 | auth.getToken(function (err) { 753 | assert.strictEqual(err, error); 754 | done(); 755 | }); 756 | }); 757 | 758 | it('should get an access token', function (done) { 759 | var fakeClient = { 760 | getAccessToken: function (callback) { 761 | callback(); 762 | } 763 | }; 764 | 765 | auth.getAuthClient = function (callback) { 766 | callback(null, fakeClient); 767 | }; 768 | 769 | auth.getToken(done); 770 | }); 771 | }); 772 | 773 | describe('isAppEngine', function () { 774 | var ENV_VARS = [ 775 | 'GAE_SERVICE', 776 | 'GAE_MODULE_NAME' 777 | ]; 778 | 779 | afterEach(function () { 780 | ENV_VARS.forEach(function (envVarName) { 781 | delete process.env[envVarName]; 782 | }); 783 | }); 784 | 785 | it('should return false without env vars sets', function (done) { 786 | auth.isAppEngine(function (err, isAppEngine) { 787 | assert.ifError(err); 788 | assert.strictEqual(isAppEngine, false); 789 | done(); 790 | }); 791 | }); 792 | 793 | it('should detect GAE_SERVICE', function (done) { 794 | process.env.GAE_SERVICE = 'service-name'; 795 | 796 | assert.strictEqual(auth.environment.IS_APP_ENGINE, undefined); 797 | 798 | auth.isAppEngine(function (err, isAppEngine) { 799 | assert.ifError(err); 800 | assert.strictEqual(auth.environment.IS_APP_ENGINE, true); 801 | assert.strictEqual(isAppEngine, true); 802 | done(); 803 | }); 804 | }); 805 | 806 | it('should detect GAE_MODULE_NAME', function (done) { 807 | process.env.GAE_MODULE_NAME = 'module-name'; 808 | 809 | assert.strictEqual(auth.environment.IS_APP_ENGINE, undefined); 810 | 811 | auth.isAppEngine(function (err, isAppEngine) { 812 | assert.ifError(err); 813 | assert.strictEqual(auth.environment.IS_APP_ENGINE, true); 814 | assert.strictEqual(isAppEngine, true); 815 | done(); 816 | }); 817 | }); 818 | }); 819 | 820 | describe('isCloudFunction', function () { 821 | var ENV_VARS = [ 822 | 'FUNCTION_NAME' 823 | ]; 824 | 825 | afterEach(function () { 826 | ENV_VARS.forEach(function (envVarName) { 827 | delete process.env[envVarName]; 828 | }); 829 | }); 830 | 831 | it('should return false without env vars sets', function (done) { 832 | auth.isCloudFunction(function (err, isCloudFunction) { 833 | assert.ifError(err); 834 | assert.strictEqual(isCloudFunction, false); 835 | done(); 836 | }); 837 | }); 838 | 839 | it('should detect FUNCTION_NAME', function (done) { 840 | process.env.FUNCTION_NAME = 'function-name'; 841 | 842 | assert.strictEqual(auth.environment.IS_CLOUD_FUNCTION, undefined); 843 | 844 | auth.isCloudFunction(function (err, isCloudFunction) { 845 | assert.ifError(err); 846 | assert.strictEqual(auth.environment.IS_CLOUD_FUNCTION, true); 847 | assert.strictEqual(isCloudFunction, true); 848 | done(); 849 | }); 850 | }); 851 | }); 852 | 853 | describe('isComputeEngine', function () { 854 | it('should return an existing value', function (done) { 855 | requestOverride = done; // will make test fail if called 856 | 857 | auth.environment.IS_COMPUTE_ENGINE = 'test'; 858 | 859 | auth.isComputeEngine(function (err, isComputeEngine) { 860 | assert.ifError(err); 861 | assert.strictEqual(isComputeEngine, 'test'); 862 | done(); 863 | }); 864 | }); 865 | 866 | it('should make the correct request', function (done) { 867 | requestOverride = function (uri) { 868 | assert.strictEqual(uri, 'http://metadata.google.internal'); 869 | done(); 870 | }; 871 | 872 | auth.isComputeEngine(assert.ifError); 873 | }); 874 | 875 | it('should set false if request errors', function (done) { 876 | requestOverride = function (uri, callback) { 877 | callback(new Error(':(')); 878 | }; 879 | 880 | assert.strictEqual(auth.environment.IS_COMPUTE_ENGINE, undefined); 881 | 882 | auth.isComputeEngine(function (err, isComputeEngine) { 883 | assert.ifError(err); 884 | assert.strictEqual(auth.environment.IS_COMPUTE_ENGINE, false); 885 | assert.strictEqual(isComputeEngine, false); 886 | done(); 887 | }); 888 | }); 889 | 890 | it('should set true if header matches', function (done) { 891 | requestOverride = function (uri, callback) { 892 | callback(null, { 893 | headers: { 894 | 'metadata-flavor': 'Google' 895 | } 896 | }); 897 | }; 898 | 899 | assert.strictEqual(auth.environment.IS_COMPUTE_ENGINE, undefined); 900 | 901 | auth.isComputeEngine(function (err, isComputeEngine) { 902 | assert.ifError(err); 903 | assert.strictEqual(auth.environment.IS_COMPUTE_ENGINE, true); 904 | assert.strictEqual(isComputeEngine, true); 905 | done(); 906 | }); 907 | }); 908 | }); 909 | 910 | describe('isContainerEngine', function () { 911 | it('should return an existing value', function (done) { 912 | instanceOverride = done; // will make test fail if called 913 | 914 | auth.environment.IS_CONTAINER_ENGINE = 'test'; 915 | 916 | auth.isContainerEngine(function (err, isContainerEngine) { 917 | assert.ifError(err); 918 | assert.strictEqual(isContainerEngine, 'test'); 919 | done(); 920 | }); 921 | }); 922 | 923 | it('should make the correct metadata lookup', function (done) { 924 | instanceOverride = function (property) { 925 | assert.strictEqual(property, '/attributes/cluster-name'); 926 | setImmediate(done); 927 | return Promise.resolve(); 928 | }; 929 | 930 | auth.isContainerEngine(assert.ifError); 931 | }); 932 | 933 | it('should set false if instance request errors', function (done) { 934 | instanceOverride = function (property, callback) { 935 | return Promise.reject(new Error(':(')); 936 | }; 937 | 938 | assert.strictEqual(auth.environment.IS_CONTAINER_ENGINE, undefined); 939 | 940 | auth.isContainerEngine(function (err, isContainerEngine) { 941 | assert.ifError(err); 942 | assert.strictEqual(auth.environment.IS_CONTAINER_ENGINE, false); 943 | assert.strictEqual(isContainerEngine, false); 944 | done(); 945 | }); 946 | }); 947 | 948 | it('should set true if instance request succeeds', function (done) { 949 | instanceOverride = function (property, callback) { 950 | return Promise.resolve(); 951 | }; 952 | 953 | assert.strictEqual(auth.environment.IS_CONTAINER_ENGINE, undefined); 954 | 955 | auth.isContainerEngine(function (err, isContainerEngine) { 956 | assert.ifError(err); 957 | assert.strictEqual(auth.environment.IS_CONTAINER_ENGINE, true); 958 | assert.strictEqual(isContainerEngine, true); 959 | done(); 960 | }); 961 | }); 962 | }); 963 | 964 | describe('sign', function () { 965 | var DATA_TO_SIGN = 'data-to-sign'; 966 | 967 | it('should return an error from getCredentials', function (done) { 968 | var error = new Error('Error.'); 969 | 970 | auth.getCredentials = function (callback) { 971 | callback(error); 972 | }; 973 | 974 | auth.sign(DATA_TO_SIGN, function (err) { 975 | assert.strictEqual(err, error); 976 | done(); 977 | }); 978 | }); 979 | 980 | it('should sign with private key if available', function (done) { 981 | auth._signWithPrivateKey = function (data, callback) { 982 | assert.strictEqual(data, DATA_TO_SIGN); 983 | callback(); // done() 984 | }; 985 | 986 | auth.getCredentials = function (callback) { 987 | callback(null, { 988 | private_key: 'private-key' 989 | }); 990 | }; 991 | 992 | auth.sign(DATA_TO_SIGN, done); 993 | }); 994 | 995 | it('should sign with API if private key is not available', function (done) { 996 | auth._signWithApi = function (data, callback) { 997 | assert.strictEqual(data, DATA_TO_SIGN); 998 | callback(); // done() 999 | }; 1000 | 1001 | auth.getCredentials = function (callback) { 1002 | callback(null, { 1003 | // private_key: 'private-key' (no private_key) 1004 | }); 1005 | }; 1006 | 1007 | auth.sign(DATA_TO_SIGN, done); 1008 | }); 1009 | }); 1010 | 1011 | describe('_signWithApi', function () { 1012 | var DATA_TO_SIGN = 'data-to-sign'; 1013 | var DEFAULT_API_RESPONSE = { 1014 | toJSON: function () { 1015 | return { 1016 | statusCode: 200 1017 | }; 1018 | } 1019 | }; 1020 | 1021 | beforeEach(function () { 1022 | auth.projectId = 'project-id'; 1023 | 1024 | auth.credentials = { 1025 | client_email: 'client-email' 1026 | }; 1027 | }); 1028 | 1029 | it('should return an error if there is no project ID', function (done) { 1030 | auth.projectId = undefined; 1031 | 1032 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1033 | assert.strictEqual(err.message, 'Cannot sign data without a project ID.'); 1034 | done(); 1035 | }); 1036 | }); 1037 | 1038 | it('should return an error if there is no client email', function (done) { 1039 | auth.credentials = { 1040 | // client_email: 'client-email' (not available) 1041 | }; 1042 | 1043 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1044 | assert.strictEqual(err.message, 'Cannot sign data without `client_email`.'); 1045 | done(); 1046 | }); 1047 | }); 1048 | 1049 | it('should authorize the signBlob request', function (done) { 1050 | auth.authorizeRequest = function (reqOpts) { 1051 | assert.deepEqual(reqOpts, { 1052 | method: 'POST', 1053 | uri: 'https://iam.googleapis.com/v1/projects/project-id/serviceAccounts/client-email:signBlob', 1054 | json: { 1055 | bytesToSign: Buffer.from(DATA_TO_SIGN).toString('base64') 1056 | } 1057 | }); 1058 | done(); 1059 | }; 1060 | 1061 | auth._signWithApi(DATA_TO_SIGN, assert.ifError); 1062 | }); 1063 | 1064 | it('should return an error from authorizing the request', function (done) { 1065 | var error = new Error('Error.'); 1066 | 1067 | auth.authorizeRequest = function (reqOpts, callback) { 1068 | callback(error); 1069 | }; 1070 | 1071 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1072 | assert.strictEqual(err, error); 1073 | done(); 1074 | }); 1075 | }); 1076 | 1077 | it('should make the authorized request', function (done) { 1078 | var authorizedReqOpts = {}; 1079 | 1080 | auth.authorizeRequest = function (reqOpts, callback) { 1081 | callback(null, authorizedReqOpts); 1082 | }; 1083 | 1084 | requestOverride = function (reqOpts) { 1085 | assert.strictEqual(reqOpts, authorizedReqOpts); 1086 | done(); 1087 | }; 1088 | 1089 | auth._signWithApi(DATA_TO_SIGN, assert.ifError); 1090 | }); 1091 | 1092 | it('should return an error from the request', function (done) { 1093 | var authorizedReqOpts = {}; 1094 | 1095 | auth.authorizeRequest = function (reqOpts, callback) { 1096 | callback(null, authorizedReqOpts); 1097 | }; 1098 | 1099 | var error = new Error('Error.'); 1100 | 1101 | requestOverride = function (reqOpts, callback) { 1102 | callback(error, DEFAULT_API_RESPONSE); 1103 | }; 1104 | 1105 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1106 | assert.strictEqual(err, error); 1107 | done(); 1108 | }); 1109 | }); 1110 | 1111 | it('should return an error in the body from the request', function (done) { 1112 | var authorizedReqOpts = {}; 1113 | 1114 | auth.authorizeRequest = function (reqOpts, callback) { 1115 | callback(null, authorizedReqOpts); 1116 | }; 1117 | 1118 | var signBlobApiError = { 1119 | message: 'Inner error', 1120 | property: 'Inner property' 1121 | }; 1122 | 1123 | var signBlobApiResponse = { 1124 | toJSON: function () { 1125 | return { 1126 | statusCode: 400, 1127 | body: { 1128 | error: signBlobApiError 1129 | } 1130 | }; 1131 | } 1132 | } 1133 | 1134 | requestOverride = function (reqOpts, callback) { 1135 | callback(null, signBlobApiResponse); 1136 | }; 1137 | 1138 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1139 | assert.strictEqual(err.message, signBlobApiError.message); 1140 | assert.strictEqual(err.property, signBlobApiError.property); 1141 | done(); 1142 | }); 1143 | }); 1144 | 1145 | it('should return a string error in the body from the request', function (done) { 1146 | var authorizedReqOpts = {}; 1147 | 1148 | auth.authorizeRequest = function (reqOpts, callback) { 1149 | callback(null, authorizedReqOpts); 1150 | }; 1151 | 1152 | var signBlobApiError = 'String error message'; 1153 | 1154 | var signBlobApiResponse = { 1155 | toJSON: function () { 1156 | return { 1157 | statusCode: 400, 1158 | body: signBlobApiError 1159 | }; 1160 | } 1161 | } 1162 | 1163 | requestOverride = function (reqOpts, callback) { 1164 | callback(null, signBlobApiResponse); 1165 | }; 1166 | 1167 | auth._signWithApi(DATA_TO_SIGN, function (err) { 1168 | assert.strictEqual(err.message, signBlobApiError); 1169 | done(); 1170 | }); 1171 | }); 1172 | 1173 | it('should return the signature', function (done) { 1174 | var authorizedReqOpts = {}; 1175 | 1176 | auth.authorizeRequest = function (reqOpts, callback) { 1177 | callback(null, authorizedReqOpts); 1178 | }; 1179 | 1180 | var body = { 1181 | signature: 'signature' 1182 | }; 1183 | 1184 | requestOverride = function (reqOpts, callback) { 1185 | callback(null, DEFAULT_API_RESPONSE, body); 1186 | }; 1187 | 1188 | auth._signWithApi(DATA_TO_SIGN, function (err, signature) { 1189 | assert.ifError(err); 1190 | assert.strictEqual(signature, body.signature); 1191 | done(); 1192 | }); 1193 | }); 1194 | }); 1195 | 1196 | describe('_signWithPrivateKey', function () { 1197 | var DATA_TO_SIGN = 'data-to-sign'; 1198 | 1199 | beforeEach(function () { 1200 | auth.credentials = { 1201 | private_key: 'private-key' 1202 | }; 1203 | }); 1204 | 1205 | it('should return the signature', function (done) { 1206 | var updatedWithCorrectData = false; 1207 | var expectedSignature = 'signed-data'; 1208 | 1209 | createSignOverride = function (algo) { 1210 | assert.strictEqual(algo, 'RSA-SHA256'); 1211 | 1212 | return { 1213 | sign: function (private_key, outputFormat) { 1214 | assert.strictEqual(private_key, auth.credentials.private_key); 1215 | return expectedSignature; 1216 | }, 1217 | 1218 | update: function (data) { 1219 | assert.strictEqual(data, DATA_TO_SIGN); 1220 | updatedWithCorrectData = true; 1221 | } 1222 | }; 1223 | }; 1224 | 1225 | auth._signWithPrivateKey(DATA_TO_SIGN, function (err, signature) { 1226 | assert.ifError(err); 1227 | assert.strictEqual(updatedWithCorrectData, true); 1228 | assert.strictEqual(signature, expectedSignature); 1229 | done(); 1230 | }); 1231 | }); 1232 | }); 1233 | }); 1234 | 1235 | describe('integration tests', function () { 1236 | var googleAutoAuth = require('./index.js'); 1237 | var SCOPES = ['https://www.googleapis.com/auth/cloud-platform']; 1238 | 1239 | var ADC = process.env.GOOGLE_APPLICATION_CREDENTIALS; 1240 | var EMAIL = process.env.P12_EMAIL; 1241 | var P12 = process.env.P12_KEYFILE; 1242 | var PEM = process.env.PEM_KEYFILE; 1243 | 1244 | before(function () { 1245 | if ((P12 || PEM) && !EMAIL) { 1246 | throw new Error('If specifying a P12 or PEM key file, you must set `P12_EMAIL` env var as well.'); 1247 | } 1248 | }); 1249 | 1250 | function testGetCredentials(authClient, callback) { 1251 | authClient.getCredentials(callback); 1252 | } 1253 | 1254 | function testAuthorizeRequest(authClient, callback) { 1255 | authClient.authorizeRequest({ 1256 | uri: 'test' 1257 | }, function (err, authenticatedReqOpts) { 1258 | if (err) { 1259 | callback(err); 1260 | return; 1261 | } 1262 | 1263 | assert(typeof authenticatedReqOpts.headers.Authorization, 'string'); 1264 | 1265 | callback(); 1266 | }); 1267 | } 1268 | 1269 | function testSign(authClient, callback) { 1270 | authClient.sign('Test', callback); 1271 | } 1272 | 1273 | function testWithAuthClient(authClient, done) { 1274 | async.parallel([ 1275 | callback => testGetCredentials(authClient, callback), 1276 | callback => testAuthorizeRequest(authClient, callback), 1277 | callback => testSign(authClient, callback) 1278 | ], done); 1279 | } 1280 | 1281 | (ADC ? it : it.skip)('should work with ADC', function (done) { 1282 | var authClient = googleAutoAuth({ scopes: SCOPES }); 1283 | 1284 | testWithAuthClient(authClient, done); 1285 | }); 1286 | 1287 | (ADC ? it : it.skip)('should work with key file path', function (done) { 1288 | var authClient = googleAutoAuth({ 1289 | scopes: SCOPES, 1290 | keyFile: ADC 1291 | }); 1292 | 1293 | testWithAuthClient(authClient, done); 1294 | }); 1295 | 1296 | (P12 ? it : it.skip)('should work with a p12 file', function (done) { 1297 | var authClient = googleAutoAuth({ 1298 | scopes: SCOPES, 1299 | email: EMAIL, 1300 | keyFile: P12 1301 | }); 1302 | 1303 | testWithAuthClient(authClient, done); 1304 | }); 1305 | 1306 | (PEM ? it : it.skip)('should work with a pem file', function (done) { 1307 | var authClient = googleAutoAuth({ 1308 | scopes: SCOPES, 1309 | email: EMAIL, 1310 | keyFile: PEM 1311 | }); 1312 | 1313 | testWithAuthClient(authClient, done); 1314 | }); 1315 | 1316 | (ADC ? it : it.skip)('should work with credentials object', function (done) { 1317 | var authClient = googleAutoAuth({ 1318 | scopes: ['https://www.googleapis.com/auth/cloud-platform'], 1319 | credentials: require(ADC) 1320 | }); 1321 | 1322 | testWithAuthClient(authClient, done); 1323 | }); 1324 | 1325 | (ADC ? it : it.skip)('should work with a provided token', function (done) { 1326 | var authClient = googleAutoAuth({scopes: SCOPES}); 1327 | 1328 | authClient.getToken(function (err, token) { 1329 | assert.ifError(err); 1330 | 1331 | var authClientWithToken = googleAutoAuth({scopes: SCOPES, token}); 1332 | testWithAuthClient(authClientWithToken, done); 1333 | }); 1334 | }); 1335 | }); 1336 | -------------------------------------------------------------------------------- /test.keyfile: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "..." 3 | } -------------------------------------------------------------------------------- /test.keyfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "..." 3 | } -------------------------------------------------------------------------------- /test.keyfile.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenplusplus/google-auto-auth/1ab3e9093c1b87200160f97db185e6cdb4046cd8/test.keyfile.pem --------------------------------------------------------------------------------