ran-boilerplate
Version:
React . Apollo (GraphQL) . Next.js Toolkit
249 lines (222 loc) • 5.78 kB
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 +
(body.error_description ? ': ' + body.error_description : ''));
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;