1 | "use strict";
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | Object.defineProperty(exports, "__esModule", { value: true });
|
9 | exports.GoogleToken = void 0;
|
10 | const fs = require("fs");
|
11 | const gaxios_1 = require("gaxios");
|
12 | const jws = require("jws");
|
13 | const path = require("path");
|
14 | const util_1 = require("util");
|
15 | const readFile = fs.readFile
|
16 | ? util_1.promisify(fs.readFile)
|
17 | : async () => {
|
18 |
|
19 | throw new ErrorWithCode('use key rather than keyFile.', 'MISSING_CREDENTIALS');
|
20 | };
|
21 | const GOOGLE_TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';
|
22 | const GOOGLE_REVOKE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/revoke?token=';
|
23 | class ErrorWithCode extends Error {
|
24 | constructor(message, code) {
|
25 | super(message);
|
26 | this.code = code;
|
27 | }
|
28 | }
|
29 | let getPem;
|
30 | class GoogleToken {
|
31 | |
32 |
|
33 |
|
34 |
|
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 |
|
53 |
|
54 |
|
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 |
|
67 |
|
68 |
|
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 |
|
98 |
|
99 |
|
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 |
|
123 |
|
124 |
|
125 |
|
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 |
|
194 |
|
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 |
|
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 | }
|
262 | exports.GoogleToken = GoogleToken;
|
263 |
|
\ | No newline at end of file |