248 lines
5.6 KiB
JavaScript
248 lines
5.6 KiB
JavaScript
var gp12pem = require('google-p12-pem');
|
|
var request = require('request');
|
|
var mime = require('mime');
|
|
var jws = require('jws');
|
|
var fs = require('fs');
|
|
|
|
var GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token';
|
|
var GOOGLE_REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke?token=';
|
|
|
|
/**
|
|
* Create a GoogleToken.
|
|
*
|
|
* @param {object} options Configuration object.
|
|
*/
|
|
function GoogleToken(options) {
|
|
if (!(this instanceof GoogleToken)) {
|
|
return new GoogleToken(options);
|
|
}
|
|
this._configure(options);
|
|
}
|
|
|
|
GoogleToken.prototype._mime = mime;
|
|
|
|
/**
|
|
* Returns whether the token has expired.
|
|
*
|
|
* @return {Boolean} true if the token has expired, false otherwise.
|
|
*/
|
|
GoogleToken.prototype.hasExpired = function() {
|
|
var now = (new Date()).getTime();
|
|
if (this.token && this.expires_at) {
|
|
return now >= this.expires_at;
|
|
} else {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
GoogleToken.prototype._gp12pem = gp12pem;
|
|
|
|
/**
|
|
* Returns a cached token or retrieves a new one from Google.
|
|
*
|
|
* @param {Function} callback The callback function.
|
|
*/
|
|
GoogleToken.prototype.getToken = function(callback) {
|
|
var self = this;
|
|
|
|
if (!this.hasExpired()) {
|
|
return callback(null, this.token);
|
|
} else {
|
|
if (!this.key && !this.keyFile) {
|
|
callback(new Error('No key or keyFile set.'));
|
|
return;
|
|
} else if (!this.key && this.keyFile) {
|
|
var mimeType = this._mime.lookup(this.keyFile);
|
|
if (mimeType === 'application/json') {
|
|
// json file
|
|
fs.readFile(this.keyFile, handleJSONKey);
|
|
} else {
|
|
// Must be a .p12 file or .pem key
|
|
if (!self.iss) {
|
|
var error = new Error('email is required.');
|
|
error.code = 'MISSING_CREDENTIALS';
|
|
callback(error);
|
|
return;
|
|
}
|
|
|
|
if (mimeType === 'application/x-pkcs12') {
|
|
// convert to .pem on the fly
|
|
self._gp12pem(this.keyFile, handleKey);
|
|
} else {
|
|
// assume .pem key otherwise
|
|
fs.readFile(this.keyFile, handleKey);
|
|
}
|
|
}
|
|
} else {
|
|
return this._requestToken(callback);
|
|
}
|
|
}
|
|
|
|
function handleJSONKey(err, key) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
try {
|
|
var body = JSON.parse(key);
|
|
self.key = body.private_key;
|
|
self.iss = body.client_email;
|
|
} catch (e) {
|
|
callback(e);
|
|
return;
|
|
}
|
|
|
|
if (!self.key || !self.iss) {
|
|
var error = new Error('private_key and client_email are required.');
|
|
error.code = 'MISSING_CREDENTIALS';
|
|
callback(error);
|
|
return;
|
|
}
|
|
|
|
self._requestToken(callback);
|
|
}
|
|
|
|
function handleKey(err, key) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
self.key = key;
|
|
self._requestToken(callback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Revoke the token if one is set.
|
|
*
|
|
* @param {Function} callback The callback function.
|
|
*/
|
|
GoogleToken.prototype.revokeToken = function(callback) {
|
|
var self = this;
|
|
if (this.token) {
|
|
this._request(GOOGLE_REVOKE_TOKEN_URL + this.token, function(err, res) {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
self._configure({
|
|
email: self.iss,
|
|
sub: self.sub,
|
|
key: self.key,
|
|
keyFile: self.keyFile,
|
|
scope: self.scope
|
|
});
|
|
callback();
|
|
});
|
|
} else {
|
|
callback(new Error('No token to revoke.'));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Configure the GoogleToken for re-use.
|
|
* @param {object} options Configuration object.
|
|
*/
|
|
GoogleToken.prototype._configure = function(options) {
|
|
var self = this;
|
|
options = options || {};
|
|
this.keyFile = options.keyFile;
|
|
this.key = options.key;
|
|
this._request = request;
|
|
this.token = this.expires_at = this.raw_token = null;
|
|
this.iss = options.email || options.iss;
|
|
|
|
if (options.sub) {
|
|
this.sub = options.sub;
|
|
}
|
|
|
|
if (typeof options.scope === 'object') {
|
|
this.scope = options.scope.join(' ');
|
|
} else {
|
|
this.scope = options.scope;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Request the token from Google.
|
|
*
|
|
* @param {Function} callback The callback function.
|
|
*/
|
|
GoogleToken.prototype._requestToken = function(callback) {
|
|
var self = this;
|
|
var iat = Math.floor(new Date().getTime() / 1000);
|
|
var payload = {
|
|
iss: this.iss,
|
|
scope: this.scope,
|
|
aud: GOOGLE_TOKEN_URL,
|
|
exp: iat + 3600, // 3600 seconds = 1 hour
|
|
iat: iat
|
|
};
|
|
|
|
if (this.sub) {
|
|
payload.sub = this.sub;
|
|
}
|
|
|
|
var toSign = {
|
|
header: {
|
|
alg: 'RS256',
|
|
typ: 'JWT'
|
|
},
|
|
payload: payload,
|
|
secret: this.key
|
|
};
|
|
|
|
return this._signJWT(toSign, function(err, signedJWT) {
|
|
if (err) {
|
|
callback(err, null);
|
|
return;
|
|
}
|
|
|
|
return self._request({
|
|
method: 'post',
|
|
url: GOOGLE_TOKEN_URL,
|
|
form: {
|
|
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
|
assertion: signedJWT
|
|
}
|
|
}, function(err, res, body) {
|
|
try {
|
|
body = JSON.parse(body);
|
|
} catch (e) {
|
|
body = {};
|
|
}
|
|
|
|
err = err || body.error && new Error(body.error);
|
|
|
|
if (err) {
|
|
self.token = null;
|
|
self.token_expires = null;
|
|
callback(err, null);
|
|
return;
|
|
}
|
|
|
|
self.raw_token = body;
|
|
self.token = body.access_token;
|
|
self.expires_at = (iat + body.expires_in) * 1000;
|
|
return callback(null, self.token);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Sign the JWT object, returning any errors in the callback.
|
|
*
|
|
* @param {object} opts The configuration object.
|
|
* @param {Function} callback The callback function.
|
|
*/
|
|
GoogleToken.prototype._signJWT = function(opts, callback) {
|
|
try {
|
|
var signedJWT = jws.sign(opts);
|
|
return callback(null, signedJWT);
|
|
} catch (err) {
|
|
callback(err, null);
|
|
}
|
|
};
|
|
|
|
module.exports = GoogleToken;
|