272 lines
9.2 KiB
JavaScript
272 lines
9.2 KiB
JavaScript
|
"use strict";
|
||
|
/**
|
||
|
* Copyright 2018 Google LLC
|
||
|
*
|
||
|
* Distributed under MIT license.
|
||
|
* See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
|
||
|
*/
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
exports.GoogleToken = void 0;
|
||
|
const fs = require("fs");
|
||
|
const gaxios_1 = require("gaxios");
|
||
|
const jws = require("jws");
|
||
|
const path = require("path");
|
||
|
const util_1 = require("util");
|
||
|
const readFile = fs.readFile
|
||
|
? (0, util_1.promisify)(fs.readFile)
|
||
|
: async () => {
|
||
|
// if running in the web-browser, fs.readFile may not have been shimmed.
|
||
|
throw new ErrorWithCode('use key rather than keyFile.', 'MISSING_CREDENTIALS');
|
||
|
};
|
||
|
const GOOGLE_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';
|
||
|
const GOOGLE_REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke?token=';
|
||
|
class ErrorWithCode extends Error {
|
||
|
constructor(message, code) {
|
||
|
super(message);
|
||
|
this.code = code;
|
||
|
}
|
||
|
}
|
||
|
let getPem;
|
||
|
class GoogleToken {
|
||
|
/**
|
||
|
* Create a GoogleToken.
|
||
|
*
|
||
|
* @param options Configuration object.
|
||
|
*/
|
||
|
constructor(options) {
|
||
|
this.transporter = {
|
||
|
request: opts => (0, gaxios_1.request)(opts),
|
||
|
};
|
||
|
this.configure(options);
|
||
|
}
|
||
|
get accessToken() {
|
||
|
return this.rawToken ? this.rawToken.access_token : undefined;
|
||
|
}
|
||
|
get idToken() {
|
||
|
return this.rawToken ? this.rawToken.id_token : undefined;
|
||
|
}
|
||
|
get tokenType() {
|
||
|
return this.rawToken ? this.rawToken.token_type : undefined;
|
||
|
}
|
||
|
get refreshToken() {
|
||
|
return this.rawToken ? this.rawToken.refresh_token : undefined;
|
||
|
}
|
||
|
/**
|
||
|
* Returns whether the token has expired.
|
||
|
*
|
||
|
* @return true if the token has expired, false otherwise.
|
||
|
*/
|
||
|
hasExpired() {
|
||
|
const now = new Date().getTime();
|
||
|
if (this.rawToken && this.expiresAt) {
|
||
|
return now >= this.expiresAt;
|
||
|
}
|
||
|
else {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Returns whether the token will expire within eagerRefreshThresholdMillis
|
||
|
*
|
||
|
* @return true if the token will be expired within eagerRefreshThresholdMillis, false otherwise.
|
||
|
*/
|
||
|
isTokenExpiring() {
|
||
|
var _a;
|
||
|
const now = new Date().getTime();
|
||
|
const eagerRefreshThresholdMillis = (_a = this.eagerRefreshThresholdMillis) !== null && _a !== void 0 ? _a : 0;
|
||
|
if (this.rawToken && this.expiresAt) {
|
||
|
return this.expiresAt <= now + eagerRefreshThresholdMillis;
|
||
|
}
|
||
|
else {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
getToken(callback, opts = {}) {
|
||
|
if (typeof callback === 'object') {
|
||
|
opts = callback;
|
||
|
callback = undefined;
|
||
|
}
|
||
|
opts = Object.assign({
|
||
|
forceRefresh: false,
|
||
|
}, opts);
|
||
|
if (callback) {
|
||
|
const cb = callback;
|
||
|
this.getTokenAsync(opts).then(t => cb(null, t), callback);
|
||
|
return;
|
||
|
}
|
||
|
return this.getTokenAsync(opts);
|
||
|
}
|
||
|
/**
|
||
|
* Given a keyFile, extract the key and client email if available
|
||
|
* @param keyFile Path to a json, pem, or p12 file that contains the key.
|
||
|
* @returns an object with privateKey and clientEmail properties
|
||
|
*/
|
||
|
async getCredentials(keyFile) {
|
||
|
const ext = path.extname(keyFile);
|
||
|
switch (ext) {
|
||
|
case '.json': {
|
||
|
const key = await readFile(keyFile, 'utf8');
|
||
|
const body = JSON.parse(key);
|
||
|
const privateKey = body.private_key;
|
||
|
const clientEmail = body.client_email;
|
||
|
if (!privateKey || !clientEmail) {
|
||
|
throw new ErrorWithCode('private_key and client_email are required.', 'MISSING_CREDENTIALS');
|
||
|
}
|
||
|
return { privateKey, clientEmail };
|
||
|
}
|
||
|
case '.der':
|
||
|
case '.crt':
|
||
|
case '.pem': {
|
||
|
const privateKey = await readFile(keyFile, 'utf8');
|
||
|
return { privateKey };
|
||
|
}
|
||
|
case '.p12':
|
||
|
case '.pfx': {
|
||
|
// NOTE: The loading of `google-p12-pem` is deferred for performance
|
||
|
// reasons. The `node-forge` npm module in `google-p12-pem` adds a fair
|
||
|
// bit time to overall module loading, and is likely not frequently
|
||
|
// used. In a future release, p12 support will be entirely removed.
|
||
|
if (!getPem) {
|
||
|
getPem = (await Promise.resolve().then(() => require('google-p12-pem'))).getPem;
|
||
|
}
|
||
|
const privateKey = await getPem(keyFile);
|
||
|
return { privateKey };
|
||
|
}
|
||
|
default:
|
||
|
throw new ErrorWithCode('Unknown certificate type. Type is determined based on file extension. ' +
|
||
|
'Current supported extensions are *.json, *.pem, and *.p12.', 'UNKNOWN_CERTIFICATE_TYPE');
|
||
|
}
|
||
|
}
|
||
|
async getTokenAsync(opts) {
|
||
|
if (this.inFlightRequest && !opts.forceRefresh) {
|
||
|
return this.inFlightRequest;
|
||
|
}
|
||
|
try {
|
||
|
return await (this.inFlightRequest = this.getTokenAsyncInner(opts));
|
||
|
}
|
||
|
finally {
|
||
|
this.inFlightRequest = undefined;
|
||
|
}
|
||
|
}
|
||
|
async getTokenAsyncInner(opts) {
|
||
|
if (this.isTokenExpiring() === false && opts.forceRefresh === false) {
|
||
|
return Promise.resolve(this.rawToken);
|
||
|
}
|
||
|
if (!this.key && !this.keyFile) {
|
||
|
throw new Error('No key or keyFile set.');
|
||
|
}
|
||
|
if (!this.key && this.keyFile) {
|
||
|
const creds = await this.getCredentials(this.keyFile);
|
||
|
this.key = creds.privateKey;
|
||
|
this.iss = creds.clientEmail || this.iss;
|
||
|
if (!creds.clientEmail) {
|
||
|
this.ensureEmail();
|
||
|
}
|
||
|
}
|
||
|
return this.requestToken();
|
||
|
}
|
||
|
ensureEmail() {
|
||
|
if (!this.iss) {
|
||
|
throw new ErrorWithCode('email is required.', 'MISSING_CREDENTIALS');
|
||
|
}
|
||
|
}
|
||
|
revokeToken(callback) {
|
||
|
if (callback) {
|
||
|
this.revokeTokenAsync().then(() => callback(), callback);
|
||
|
return;
|
||
|
}
|
||
|
return this.revokeTokenAsync();
|
||
|
}
|
||
|
async revokeTokenAsync() {
|
||
|
if (!this.accessToken) {
|
||
|
throw new Error('No token to revoke.');
|
||
|
}
|
||
|
const url = GOOGLE_REVOKE_TOKEN_URL + this.accessToken;
|
||
|
await this.transporter.request({ url });
|
||
|
this.configure({
|
||
|
email: this.iss,
|
||
|
sub: this.sub,
|
||
|
key: this.key,
|
||
|
keyFile: this.keyFile,
|
||
|
scope: this.scope,
|
||
|
additionalClaims: this.additionalClaims,
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Configure the GoogleToken for re-use.
|
||
|
* @param {object} options Configuration object.
|
||
|
*/
|
||
|
configure(options = {}) {
|
||
|
this.keyFile = options.keyFile;
|
||
|
this.key = options.key;
|
||
|
this.rawToken = undefined;
|
||
|
this.iss = options.email || options.iss;
|
||
|
this.sub = options.sub;
|
||
|
this.additionalClaims = options.additionalClaims;
|
||
|
if (typeof options.scope === 'object') {
|
||
|
this.scope = options.scope.join(' ');
|
||
|
}
|
||
|
else {
|
||
|
this.scope = options.scope;
|
||
|
}
|
||
|
this.eagerRefreshThresholdMillis = options.eagerRefreshThresholdMillis;
|
||
|
if (options.transporter) {
|
||
|
this.transporter = options.transporter;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Request the token from Google.
|
||
|
*/
|
||
|
async requestToken() {
|
||
|
var _a, _b;
|
||
|
const iat = Math.floor(new Date().getTime() / 1000);
|
||
|
const additionalClaims = this.additionalClaims || {};
|
||
|
const payload = Object.assign({
|
||
|
iss: this.iss,
|
||
|
scope: this.scope,
|
||
|
aud: GOOGLE_TOKEN_URL,
|
||
|
exp: iat + 3600,
|
||
|
iat,
|
||
|
sub: this.sub,
|
||
|
}, additionalClaims);
|
||
|
const signedJWT = jws.sign({
|
||
|
header: { alg: 'RS256' },
|
||
|
payload,
|
||
|
secret: this.key,
|
||
|
});
|
||
|
try {
|
||
|
const r = await this.transporter.request({
|
||
|
method: 'POST',
|
||
|
url: GOOGLE_TOKEN_URL,
|
||
|
data: {
|
||
|
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||
|
assertion: signedJWT,
|
||
|
},
|
||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||
|
responseType: 'json',
|
||
|
});
|
||
|
this.rawToken = r.data;
|
||
|
this.expiresAt =
|
||
|
r.data.expires_in === null || r.data.expires_in === undefined
|
||
|
? undefined
|
||
|
: (iat + r.data.expires_in) * 1000;
|
||
|
return this.rawToken;
|
||
|
}
|
||
|
catch (e) {
|
||
|
this.rawToken = undefined;
|
||
|
this.tokenExpires = undefined;
|
||
|
const body = e.response && ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data)
|
||
|
? (_b = e.response) === null || _b === void 0 ? void 0 : _b.data
|
||
|
: {};
|
||
|
if (body.error) {
|
||
|
const desc = body.error_description
|
||
|
? `: ${body.error_description}`
|
||
|
: '';
|
||
|
e.message = `${body.error}${desc}`;
|
||
|
}
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.GoogleToken = GoogleToken;
|
||
|
//# sourceMappingURL=index.js.map
|