UNPKG

9.09 kBJavaScriptView Raw
1"use strict";
2/**
3 * Copyright 2018 Google LLC
4 *
5 * Distributed under MIT license.
6 * See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
7 */
8Object.defineProperty(exports, "__esModule", { value: true });
9exports.GoogleToken = void 0;
10const fs = require("fs");
11const gaxios_1 = require("gaxios");
12const jws = require("jws");
13const path = require("path");
14const util_1 = require("util");
15const readFile = fs.readFile
16 ? util_1.promisify(fs.readFile)
17 : async () => {
18 // if running in the web-browser, fs.readFile may not have been shimmed.
19 throw new ErrorWithCode('use key rather than keyFile.', 'MISSING_CREDENTIALS');
20 };
21const GOOGLE_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';
22const GOOGLE_REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke?token=';
23class ErrorWithCode extends Error {
24 constructor(message, code) {
25 super(message);
26 this.code = code;
27 }
28}
29let getPem;
30class GoogleToken {
31 /**
32 * Create a GoogleToken.
33 *
34 * @param options Configuration object.
35 */
36 constructor(options) {
37 this.configure(options);
38 }
39 get accessToken() {
40 return this.rawToken ? this.rawToken.access_token : undefined;
41 }
42 get idToken() {
43 return this.rawToken ? this.rawToken.id_token : undefined;
44 }
45 get tokenType() {
46 return this.rawToken ? this.rawToken.token_type : undefined;
47 }
48 get refreshToken() {
49 return this.rawToken ? this.rawToken.refresh_token : undefined;
50 }
51 /**
52 * Returns whether the token has expired.
53 *
54 * @return true if the token has expired, false otherwise.
55 */
56 hasExpired() {
57 const now = new Date().getTime();
58 if (this.rawToken && this.expiresAt) {
59 return now >= this.expiresAt;
60 }
61 else {
62 return true;
63 }
64 }
65 /**
66 * Returns whether the token will expire within eagerRefreshThresholdMillis
67 *
68 * @return true if the token will be expired within eagerRefreshThresholdMillis, false otherwise.
69 */
70 isTokenExpiring() {
71 var _a;
72 const now = new Date().getTime();
73 const eagerRefreshThresholdMillis = (_a = this.eagerRefreshThresholdMillis) !== null && _a !== void 0 ? _a : 0;
74 if (this.rawToken && this.expiresAt) {
75 return this.expiresAt <= now + eagerRefreshThresholdMillis;
76 }
77 else {
78 return true;
79 }
80 }
81 getToken(callback, opts = {}) {
82 if (typeof callback === 'object') {
83 opts = callback;
84 callback = undefined;
85 }
86 opts = Object.assign({
87 forceRefresh: false,
88 }, opts);
89 if (callback) {
90 const cb = callback;
91 this.getTokenAsync(opts).then(t => cb(null, t), callback);
92 return;
93 }
94 return this.getTokenAsync(opts);
95 }
96 /**
97 * Given a keyFile, extract the key and client email if available
98 * @param keyFile Path to a json, pem, or p12 file that contains the key.
99 * @returns an object with privateKey and clientEmail properties
100 */
101 async getCredentials(keyFile) {
102 const ext = path.extname(keyFile);
103 switch (ext) {
104 case '.json': {
105 const key = await readFile(keyFile, 'utf8');
106 const body = JSON.parse(key);
107 const privateKey = body.private_key;
108 const clientEmail = body.client_email;
109 if (!privateKey || !clientEmail) {
110 throw new ErrorWithCode('private_key and client_email are required.', 'MISSING_CREDENTIALS');
111 }
112 return { privateKey, clientEmail };
113 }
114 case '.der':
115 case '.crt':
116 case '.pem': {
117 const privateKey = await readFile(keyFile, 'utf8');
118 return { privateKey };
119 }
120 case '.p12':
121 case '.pfx': {
122 // NOTE: The loading of `google-p12-pem` is deferred for performance
123 // reasons. The `node-forge` npm module in `google-p12-pem` adds a fair
124 // bit time to overall module loading, and is likely not frequently
125 // used. In a future release, p12 support will be entirely removed.
126 if (!getPem) {
127 getPem = (await Promise.resolve().then(() => require('google-p12-pem'))).getPem;
128 }
129 const privateKey = await getPem(keyFile);
130 return { privateKey };
131 }
132 default:
133 throw new ErrorWithCode('Unknown certificate type. Type is determined based on file extension. ' +
134 'Current supported extensions are *.json, *.pem, and *.p12.', 'UNKNOWN_CERTIFICATE_TYPE');
135 }
136 }
137 async getTokenAsync(opts) {
138 if (this.inFlightRequest && !opts.forceRefresh) {
139 return this.inFlightRequest;
140 }
141 try {
142 return await (this.inFlightRequest = this.getTokenAsyncInner(opts));
143 }
144 finally {
145 this.inFlightRequest = undefined;
146 }
147 }
148 async getTokenAsyncInner(opts) {
149 if (this.isTokenExpiring() === false && opts.forceRefresh === false) {
150 return Promise.resolve(this.rawToken);
151 }
152 if (!this.key && !this.keyFile) {
153 throw new Error('No key or keyFile set.');
154 }
155 if (!this.key && this.keyFile) {
156 const creds = await this.getCredentials(this.keyFile);
157 this.key = creds.privateKey;
158 this.iss = creds.clientEmail || this.iss;
159 if (!creds.clientEmail) {
160 this.ensureEmail();
161 }
162 }
163 return this.requestToken();
164 }
165 ensureEmail() {
166 if (!this.iss) {
167 throw new ErrorWithCode('email is required.', 'MISSING_CREDENTIALS');
168 }
169 }
170 revokeToken(callback) {
171 if (callback) {
172 this.revokeTokenAsync().then(() => callback(), callback);
173 return;
174 }
175 return this.revokeTokenAsync();
176 }
177 async revokeTokenAsync() {
178 if (!this.accessToken) {
179 throw new Error('No token to revoke.');
180 }
181 const url = GOOGLE_REVOKE_TOKEN_URL + this.accessToken;
182 await gaxios_1.request({ url });
183 this.configure({
184 email: this.iss,
185 sub: this.sub,
186 key: this.key,
187 keyFile: this.keyFile,
188 scope: this.scope,
189 additionalClaims: this.additionalClaims,
190 });
191 }
192 /**
193 * Configure the GoogleToken for re-use.
194 * @param {object} options Configuration object.
195 */
196 configure(options = {}) {
197 this.keyFile = options.keyFile;
198 this.key = options.key;
199 this.rawToken = undefined;
200 this.iss = options.email || options.iss;
201 this.sub = options.sub;
202 this.additionalClaims = options.additionalClaims;
203 if (typeof options.scope === 'object') {
204 this.scope = options.scope.join(' ');
205 }
206 else {
207 this.scope = options.scope;
208 }
209 this.eagerRefreshThresholdMillis = options.eagerRefreshThresholdMillis;
210 }
211 /**
212 * Request the token from Google.
213 */
214 async requestToken() {
215 const iat = Math.floor(new Date().getTime() / 1000);
216 const additionalClaims = this.additionalClaims || {};
217 const payload = Object.assign({
218 iss: this.iss,
219 scope: this.scope,
220 aud: GOOGLE_TOKEN_URL,
221 exp: iat + 3600,
222 iat,
223 sub: this.sub,
224 }, additionalClaims);
225 const signedJWT = jws.sign({
226 header: { alg: 'RS256' },
227 payload,
228 secret: this.key,
229 });
230 try {
231 const r = await gaxios_1.request({
232 method: 'POST',
233 url: GOOGLE_TOKEN_URL,
234 data: {
235 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
236 assertion: signedJWT,
237 },
238 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
239 responseType: 'json',
240 });
241 this.rawToken = r.data;
242 this.expiresAt =
243 r.data.expires_in === null || r.data.expires_in === undefined
244 ? undefined
245 : (iat + r.data.expires_in) * 1000;
246 return this.rawToken;
247 }
248 catch (e) {
249 this.rawToken = undefined;
250 this.tokenExpires = undefined;
251 const body = e.response && e.response.data ? e.response.data : {};
252 if (body.error) {
253 const desc = body.error_description
254 ? `: ${body.error_description}`
255 : '';
256 e.message = `${body.error}${desc}`;
257 }
258 throw e;
259 }
260 }
261}
262exports.GoogleToken = GoogleToken;
263//# sourceMappingURL=index.js.map
\No newline at end of file