├── .gitignore ├── .travis.yml ├── README.md ├── gapitoken.js ├── package.json └── test ├── auth-with-google.test.js └── test-key.pem /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-gapitoken 2 | ============== 3 | 4 | Node.js module for Google API service account authorization (Server to Server flow). 5 | 6 | [![Build Status](https://travis-ci.org/bsphere/node-gapitoken.svg)](https://travis-ci.org/bsphere/node-gapitoken) 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | npm install gapitoken 13 | 14 | Usage 15 | ----- 16 | 17 | var GAPI = require('gapitoken'); 18 | 19 | var gapi = new GAPI({ 20 | iss: 'service account email address from Google API console', 21 | scope: 'space delimited list of requested scopes', 22 | keyFile: 'path to private_key.pem' 23 | }, function(err) { 24 | if (err) { return console.log(err); } 25 | 26 | gapi.getToken(function(err, token) { 27 | if (err) { return console.log(err); } 28 | console.log(token); 29 | }); 30 | }); 31 | 32 | Another option is to pass the private key as a string 33 | 34 | var key = "-----BEGIN RSA PRIVATE KEY-----\n\ 35 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 36 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 37 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 38 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 39 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 40 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n\ 41 | -----END RSA PRIVATE KEY-----"; 42 | 43 | var gapi = new GAPI({ 44 | iss: 'service account email address from Google API console', 45 | scope: 'space delimited list of requested scopes', 46 | key: key 47 | }, function() { 48 | gapi.getToken(function(err, token) { 49 | if (err) { return console.log(err); } 50 | console.log(token); 51 | }); 52 | }); 53 | 54 | 55 | * for using node-gapitoken to access Google Cloud Storage see https://github.com/bsphere/node-gcs 56 | 57 | Creating a Private key file 58 | --------------------------- 59 | 60 | 1) Login to Google API Console, and under "API Access" create a "service account" for your project. 61 | 62 | 2) Download the .p12 private key file 63 | 64 | 3) Convert the .p12 file to .pem: `openssl pkcs12 -in key.p12 -out key.pem -nocerts` 65 | 66 | NOTE: You must set a passphrase for the .pem file 67 | 68 | 4) Remove the passphrase from the .pem file: `openssl rsa -in key.pem -out key.pem` 69 | -------------------------------------------------------------------------------- /gapitoken.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jws = require('jws'); 4 | var fs = require('fs'); 5 | var request = require('request'); 6 | 7 | var GAPI = function(options, callback) { 8 | this.token = null; 9 | this.token_expires = null; 10 | 11 | this.iss = options.iss; 12 | this.scope = options.scope; 13 | this.sub = options.sub; 14 | this.prn = options.prn; 15 | 16 | if (options.keyFile) { 17 | var self = this; 18 | process.nextTick(function() { 19 | fs.readFile(options.keyFile, function(err, res) { 20 | if (err) { return callback(err); } 21 | self.key = res; 22 | callback(); 23 | }); 24 | }); 25 | } else if (options.key) { 26 | this.key = options.key; 27 | process.nextTick(callback); 28 | } else { 29 | callback(new Error("Missing key, key or keyFile option must be provided!")); 30 | } 31 | }; 32 | 33 | GAPI.prototype.getToken = function(callback) { 34 | if (this.token && this.token_expires && (new Date()).getTime() < this.token_expires * 1000) { 35 | callback(null, this.token); 36 | } else { 37 | this.getAccessToken(callback); 38 | } 39 | }; 40 | 41 | GAPI.prototype.getAccessToken = function(callback) { 42 | var self = this; 43 | var iat = Math.floor(new Date().getTime() / 1000); 44 | 45 | var payload = { 46 | iss: this.iss, 47 | scope: this.scope, 48 | aud: 'https://accounts.google.com/o/oauth2/token', 49 | exp: iat + 3600, 50 | iat: iat 51 | }; 52 | 53 | if(this.sub) 54 | payload.sub = this.sub; 55 | 56 | if(this.prn) 57 | payload.prn = this.prn; 58 | 59 | var signedJWT = jws.sign({ 60 | header: {alg: 'RS256', typ: 'JWT'}, 61 | payload: payload, 62 | secret: this.key 63 | }); 64 | 65 | var post_options = { 66 | url: 'https://accounts.google.com/o/oauth2/token', 67 | method: 'POST', 68 | strictSSL: false, 69 | form: { 70 | 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 71 | 'assertion': signedJWT 72 | }, 73 | headers: { 74 | 'Content-Type': 'application/x-www-form-urlencoded' 75 | } 76 | }; 77 | 78 | request(post_options, function(error, response, body) { 79 | if(error){ 80 | self.token = null; 81 | self.token_expires = null; 82 | callback(error, null); 83 | } else { 84 | try { 85 | var d = JSON.parse(body); 86 | if (d.error) { 87 | self.token = null; 88 | self.token_expires = null; 89 | callback(d.error, null); 90 | } else { 91 | self.token = d.access_token; 92 | self.token_expires = iat + 3600; 93 | callback(null, self.token); 94 | } 95 | } catch (e) { 96 | callback(e, null); 97 | } 98 | } 99 | }); 100 | }; 101 | 102 | module.exports = GAPI; 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gapitoken", 3 | "version": "0.1.5", 4 | "description": "Node.js module for Google API service account authorization (Server to Server flow)", 5 | "main": "gapitoken.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/bsphere/node-gapitoken.git" 9 | }, 10 | "keywords": [ 11 | "jwt", 12 | "jws", 13 | "google", 14 | "api", 15 | "token", 16 | "service" 17 | ], 18 | "author": { 19 | "name": "Gal Ben-Haim" 20 | }, 21 | "license": "MIT", 22 | "readmeFilename": "README.md", 23 | "dependencies": { 24 | "jws": "~3.0.0", 25 | "request": "^2.54.0" 26 | }, 27 | "devDependencies": { 28 | "mocha": "^2.2.5" 29 | }, 30 | "scripts": { 31 | "test": "mocha test" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/auth-with-google.test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | var GAPI = require('../gapitoken.js'); 5 | 6 | var pemPath = path.join(__dirname, 'test-key.pem'); 7 | 8 | describe('Authenticating with Google', function() { 9 | it('should work with a .pem file', function(done) { 10 | var gapi = new GAPI({ 11 | iss: "985952909795-p560igbg1r2hjaagrpeust4sqca9vhi8@developer.gserviceaccount.com", 12 | scope: 'https://www.googleapis.com/auth/bigquery', 13 | keyFile: pemPath 14 | }, function(error) { 15 | if (error) { return done(error); } 16 | 17 | gapi.getToken(function(error, token) { 18 | assert.ok(token, "Got a token"); 19 | done(error); 20 | }); 21 | }); 22 | }); 23 | 24 | it('should work with an RSA string', function(done) { 25 | var gapi = new GAPI({ 26 | iss: "985952909795-p560igbg1r2hjaagrpeust4sqca9vhi8@developer.gserviceaccount.com", 27 | scope: 'https://www.googleapis.com/auth/bigquery', 28 | key: fs.readFileSync(pemPath) 29 | }, function(error) { 30 | if (error) { return done(error); } 31 | 32 | gapi.getToken(function(error, token) { 33 | assert.ok(token, "Got a token"); 34 | done(error); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/test-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAzs0L7d8UdOt5ifzb23NVSp7u1T1cOh+aivtmgyMCk1r/Ua2E 3 | UBtf8MxK2Gxjw010hIOXDU7u3/opNx48/2Pe9ZFItp2NtZJjzzuttP0RLpNIkiPv 4 | H5r42TrtGx01Z7lj1y/thfMEk7kM/zjSdTOTq2hMExPwXPC/A06rih2RZchNXPEF 5 | ZREHDjL4tD51vnQpT5h7kC7UZFaeHOwT4V7YxuwvJgZhVcGE4FPb7yR1SlD1VLKg 6 | RqnwToIQI5stCIoiyq+tiRiNyANQ1/pnux54w4twTFVZMa0rlt56EYrN/lnzS3rH 7 | nwDbYpuZJ8RG9XFyPkvM6rkFPMkGr1v5DN0KtQIDAQABAoIBAQCIVStJHSj6T4an 8 | jnhOu0D+Wbqv/6y+cLlMaKJTT5BSHsp1SUQSH4nnUHcDKKm4RmbLOYkqb+AQ7nWf 9 | baLe5zNEhwv9v+59Rk3gf8Jrr+5U2yoble790ge+BhfsKRxdDh4g7erTxLs+u98y 10 | nn44X9E5Th/g7+MlEl2Ky6FRA18oTm4kIFdWeWARisnu2BlTgN6zgLjydtDMsKtU 11 | 0szJQciTKTsIGa9ubxW50c5+eHq5ui3f3O/PsQsSdF0/N+UCUkon9HKv9hOPhVLU 12 | uUf26rH2TYtXk2Jj1FSm2FVg2df/v1IkKdyETxVZlkApfrDVAzf2ekUBxP+F8vIj 13 | TVeKUlfdAoGBAOudczQ0FsNH7nHviv6yazEWgQCw5u0zfQl1fV3OhzackdX7ezs6 14 | c/yZJDJJV96ngCVQG+99q/vyxAU1vot1oUxpSAzr+T84qbQOzKXvkWXixjtU44Pc 15 | 89xmBliEAV34Z3V6iutTdEPYf7VzzLDbpEokuBJDCLbaE7GUBcrEOKyvAoGBAOCx 16 | Z4GA/OFndXFBiKd26dHFnLle2kpe07btuo5+HxAvhVi809q1D0303Rk5ZaXndEfL 17 | 4RiCN0PNEN+Wpz/S6wntuoqZXHVLrzrmji+r7hmrGLUd8UwbZ5195u/Z0rE1j4FM 18 | HePCiUsqQ75+dozpmWli0U0k/uKO8YI4WAG3Uf/bAoGBAMsCuwS77PENxXH+R/D0 19 | ok+FaFnEEvVGa/H+faqaDP8NIMsTCDccQlvfR8TcVQ3PnSaJQ8iyq8FGh4dFq4xW 20 | KkjAXghkOjozBGXUaph/NvRm1Q7CMdaoRqYcclNKSjNBmUMr9w6UtZTPshoSUMgC 21 | OCh6Qu/ZOmTyug1D0UnIkFyZAoGAHcwI8OidIHsq3OkW6TZgzZvKK98DTceDhgfC 22 | rLDXB7t1kI4B/kfyjqEBTR4Kd9TY28iSwkBKeehQ64W8Cj+7QabQDR9HXpq50zLN 23 | +k5vleHVtfcRj8k2lXEj1fzp7uwuarQgZprewCQLtdBpgkhcOtiK7xMP5hWUu3Mj 24 | YJ7h9e8CgYEACF5T9VqSAkbD89qN2bRoXVl2xFmreW2Vjp62AybsEhAd/wulOORp 25 | ny9uYkWDRSuBDRxhajhMGX55hS74De8XFC4hf6ScjZe+VUqRWbG6rSdnb4iOPoiQ 26 | XBJEa/7N7livS2QxVEQeaCPSVOIwyp523Qtvs3KojHTd0fmMGgj5imY= 27 | -----END RSA PRIVATE KEY----- 28 | --------------------------------------------------------------------------------