UNPKG

31.7 kBJavaScriptView Raw
1"use strict";
2// Copyright 2019 Google LLC
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15Object.defineProperty(exports, "__esModule", { value: true });
16exports.OAuth2Client = exports.CertificateFormat = exports.CodeChallengeMethod = void 0;
17const gaxios_1 = require("gaxios");
18const querystring = require("querystring");
19const stream = require("stream");
20const formatEcdsa = require("ecdsa-sig-formatter");
21const crypto_1 = require("../crypto/crypto");
22const authclient_1 = require("./authclient");
23const loginticket_1 = require("./loginticket");
24var CodeChallengeMethod;
25(function (CodeChallengeMethod) {
26 CodeChallengeMethod["Plain"] = "plain";
27 CodeChallengeMethod["S256"] = "S256";
28})(CodeChallengeMethod || (exports.CodeChallengeMethod = CodeChallengeMethod = {}));
29var CertificateFormat;
30(function (CertificateFormat) {
31 CertificateFormat["PEM"] = "PEM";
32 CertificateFormat["JWK"] = "JWK";
33})(CertificateFormat || (exports.CertificateFormat = CertificateFormat = {}));
34class OAuth2Client extends authclient_1.AuthClient {
35 constructor(optionsOrClientId, clientSecret, redirectUri) {
36 const opts = optionsOrClientId && typeof optionsOrClientId === 'object'
37 ? optionsOrClientId
38 : { clientId: optionsOrClientId, clientSecret, redirectUri };
39 super(opts);
40 this.certificateCache = {};
41 this.certificateExpiry = null;
42 this.certificateCacheFormat = CertificateFormat.PEM;
43 this.refreshTokenPromises = new Map();
44 this._clientId = opts.clientId;
45 this._clientSecret = opts.clientSecret;
46 this.redirectUri = opts.redirectUri;
47 this.endpoints = {
48 tokenInfoUrl: 'https://oauth2.googleapis.com/tokeninfo',
49 oauth2AuthBaseUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
50 oauth2TokenUrl: 'https://oauth2.googleapis.com/token',
51 oauth2RevokeUrl: 'https://oauth2.googleapis.com/revoke',
52 oauth2FederatedSignonPemCertsUrl: 'https://www.googleapis.com/oauth2/v1/certs',
53 oauth2FederatedSignonJwkCertsUrl: 'https://www.googleapis.com/oauth2/v3/certs',
54 oauth2IapPublicKeyUrl: 'https://www.gstatic.com/iap/verify/public_key',
55 ...opts.endpoints,
56 };
57 this.issuers = opts.issuers || [
58 'accounts.google.com',
59 'https://accounts.google.com',
60 this.universeDomain,
61 ];
62 }
63 /**
64 * Generates URL for consent page landing.
65 * @param opts Options.
66 * @return URL to consent page.
67 */
68 generateAuthUrl(opts = {}) {
69 if (opts.code_challenge_method && !opts.code_challenge) {
70 throw new Error('If a code_challenge_method is provided, code_challenge must be included.');
71 }
72 opts.response_type = opts.response_type || 'code';
73 opts.client_id = opts.client_id || this._clientId;
74 opts.redirect_uri = opts.redirect_uri || this.redirectUri;
75 // Allow scopes to be passed either as array or a string
76 if (Array.isArray(opts.scope)) {
77 opts.scope = opts.scope.join(' ');
78 }
79 const rootUrl = this.endpoints.oauth2AuthBaseUrl.toString();
80 return (rootUrl +
81 '?' +
82 querystring.stringify(opts));
83 }
84 generateCodeVerifier() {
85 // To make the code compatible with browser SubtleCrypto we need to make
86 // this method async.
87 throw new Error('generateCodeVerifier is removed, please use generateCodeVerifierAsync instead.');
88 }
89 /**
90 * Convenience method to automatically generate a code_verifier, and its
91 * resulting SHA256. If used, this must be paired with a S256
92 * code_challenge_method.
93 *
94 * For a full example see:
95 * https://github.com/googleapis/google-auth-library-nodejs/blob/main/samples/oauth2-codeVerifier.js
96 */
97 async generateCodeVerifierAsync() {
98 // base64 encoding uses 6 bits per character, and we want to generate128
99 // characters. 6*128/8 = 96.
100 const crypto = (0, crypto_1.createCrypto)();
101 const randomString = crypto.randomBytesBase64(96);
102 // The valid characters in the code_verifier are [A-Z]/[a-z]/[0-9]/
103 // "-"/"."/"_"/"~". Base64 encoded strings are pretty close, so we're just
104 // swapping out a few chars.
105 const codeVerifier = randomString
106 .replace(/\+/g, '~')
107 .replace(/=/g, '_')
108 .replace(/\//g, '-');
109 // Generate the base64 encoded SHA256
110 const unencodedCodeChallenge = await crypto.sha256DigestBase64(codeVerifier);
111 // We need to use base64UrlEncoding instead of standard base64
112 const codeChallenge = unencodedCodeChallenge
113 .split('=')[0]
114 .replace(/\+/g, '-')
115 .replace(/\//g, '_');
116 return { codeVerifier, codeChallenge };
117 }
118 getToken(codeOrOptions, callback) {
119 const options = typeof codeOrOptions === 'string' ? { code: codeOrOptions } : codeOrOptions;
120 if (callback) {
121 this.getTokenAsync(options).then(r => callback(null, r.tokens, r.res), e => callback(e, null, e.response));
122 }
123 else {
124 return this.getTokenAsync(options);
125 }
126 }
127 async getTokenAsync(options) {
128 const url = this.endpoints.oauth2TokenUrl.toString();
129 const values = {
130 code: options.code,
131 client_id: options.client_id || this._clientId,
132 client_secret: this._clientSecret,
133 redirect_uri: options.redirect_uri || this.redirectUri,
134 grant_type: 'authorization_code',
135 code_verifier: options.codeVerifier,
136 };
137 const res = await this.transporter.request({
138 ...OAuth2Client.RETRY_CONFIG,
139 method: 'POST',
140 url,
141 data: querystring.stringify(values),
142 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
143 });
144 const tokens = res.data;
145 if (res.data && res.data.expires_in) {
146 tokens.expiry_date = new Date().getTime() + res.data.expires_in * 1000;
147 delete tokens.expires_in;
148 }
149 this.emit('tokens', tokens);
150 return { tokens, res };
151 }
152 /**
153 * Refreshes the access token.
154 * @param refresh_token Existing refresh token.
155 * @private
156 */
157 async refreshToken(refreshToken) {
158 if (!refreshToken) {
159 return this.refreshTokenNoCache(refreshToken);
160 }
161 // If a request to refresh using the same token has started,
162 // return the same promise.
163 if (this.refreshTokenPromises.has(refreshToken)) {
164 return this.refreshTokenPromises.get(refreshToken);
165 }
166 const p = this.refreshTokenNoCache(refreshToken).then(r => {
167 this.refreshTokenPromises.delete(refreshToken);
168 return r;
169 }, e => {
170 this.refreshTokenPromises.delete(refreshToken);
171 throw e;
172 });
173 this.refreshTokenPromises.set(refreshToken, p);
174 return p;
175 }
176 async refreshTokenNoCache(refreshToken) {
177 var _a;
178 if (!refreshToken) {
179 throw new Error('No refresh token is set.');
180 }
181 const url = this.endpoints.oauth2TokenUrl.toString();
182 const data = {
183 refresh_token: refreshToken,
184 client_id: this._clientId,
185 client_secret: this._clientSecret,
186 grant_type: 'refresh_token',
187 };
188 let res;
189 try {
190 // request for new token
191 res = await this.transporter.request({
192 ...OAuth2Client.RETRY_CONFIG,
193 method: 'POST',
194 url,
195 data: querystring.stringify(data),
196 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
197 });
198 }
199 catch (e) {
200 if (e instanceof gaxios_1.GaxiosError &&
201 e.message === 'invalid_grant' &&
202 ((_a = e.response) === null || _a === void 0 ? void 0 : _a.data) &&
203 /ReAuth/i.test(e.response.data.error_description)) {
204 e.message = JSON.stringify(e.response.data);
205 }
206 throw e;
207 }
208 const tokens = res.data;
209 // TODO: de-duplicate this code from a few spots
210 if (res.data && res.data.expires_in) {
211 tokens.expiry_date = new Date().getTime() + res.data.expires_in * 1000;
212 delete tokens.expires_in;
213 }
214 this.emit('tokens', tokens);
215 return { tokens, res };
216 }
217 refreshAccessToken(callback) {
218 if (callback) {
219 this.refreshAccessTokenAsync().then(r => callback(null, r.credentials, r.res), callback);
220 }
221 else {
222 return this.refreshAccessTokenAsync();
223 }
224 }
225 async refreshAccessTokenAsync() {
226 const r = await this.refreshToken(this.credentials.refresh_token);
227 const tokens = r.tokens;
228 tokens.refresh_token = this.credentials.refresh_token;
229 this.credentials = tokens;
230 return { credentials: this.credentials, res: r.res };
231 }
232 getAccessToken(callback) {
233 if (callback) {
234 this.getAccessTokenAsync().then(r => callback(null, r.token, r.res), callback);
235 }
236 else {
237 return this.getAccessTokenAsync();
238 }
239 }
240 async getAccessTokenAsync() {
241 const shouldRefresh = !this.credentials.access_token || this.isTokenExpiring();
242 if (shouldRefresh) {
243 if (!this.credentials.refresh_token) {
244 if (this.refreshHandler) {
245 const refreshedAccessToken = await this.processAndValidateRefreshHandler();
246 if (refreshedAccessToken === null || refreshedAccessToken === void 0 ? void 0 : refreshedAccessToken.access_token) {
247 this.setCredentials(refreshedAccessToken);
248 return { token: this.credentials.access_token };
249 }
250 }
251 else {
252 throw new Error('No refresh token or refresh handler callback is set.');
253 }
254 }
255 const r = await this.refreshAccessTokenAsync();
256 if (!r.credentials || (r.credentials && !r.credentials.access_token)) {
257 throw new Error('Could not refresh access token.');
258 }
259 return { token: r.credentials.access_token, res: r.res };
260 }
261 else {
262 return { token: this.credentials.access_token };
263 }
264 }
265 /**
266 * The main authentication interface. It takes an optional url which when
267 * present is the endpoint being accessed, and returns a Promise which
268 * resolves with authorization header fields.
269 *
270 * In OAuth2Client, the result has the form:
271 * { Authorization: 'Bearer <access_token_value>' }
272 * @param url The optional url being authorized
273 */
274 async getRequestHeaders(url) {
275 const headers = (await this.getRequestMetadataAsync(url)).headers;
276 return headers;
277 }
278 async getRequestMetadataAsync(
279 // eslint-disable-next-line @typescript-eslint/no-unused-vars
280 url) {
281 const thisCreds = this.credentials;
282 if (!thisCreds.access_token &&
283 !thisCreds.refresh_token &&
284 !this.apiKey &&
285 !this.refreshHandler) {
286 throw new Error('No access, refresh token, API key or refresh handler callback is set.');
287 }
288 if (thisCreds.access_token && !this.isTokenExpiring()) {
289 thisCreds.token_type = thisCreds.token_type || 'Bearer';
290 const headers = {
291 Authorization: thisCreds.token_type + ' ' + thisCreds.access_token,
292 };
293 return { headers: this.addSharedMetadataHeaders(headers) };
294 }
295 // If refreshHandler exists, call processAndValidateRefreshHandler().
296 if (this.refreshHandler) {
297 const refreshedAccessToken = await this.processAndValidateRefreshHandler();
298 if (refreshedAccessToken === null || refreshedAccessToken === void 0 ? void 0 : refreshedAccessToken.access_token) {
299 this.setCredentials(refreshedAccessToken);
300 const headers = {
301 Authorization: 'Bearer ' + this.credentials.access_token,
302 };
303 return { headers: this.addSharedMetadataHeaders(headers) };
304 }
305 }
306 if (this.apiKey) {
307 return { headers: { 'X-Goog-Api-Key': this.apiKey } };
308 }
309 let r = null;
310 let tokens = null;
311 try {
312 r = await this.refreshToken(thisCreds.refresh_token);
313 tokens = r.tokens;
314 }
315 catch (err) {
316 const e = err;
317 if (e.response &&
318 (e.response.status === 403 || e.response.status === 404)) {
319 e.message = `Could not refresh access token: ${e.message}`;
320 }
321 throw e;
322 }
323 const credentials = this.credentials;
324 credentials.token_type = credentials.token_type || 'Bearer';
325 tokens.refresh_token = credentials.refresh_token;
326 this.credentials = tokens;
327 const headers = {
328 Authorization: credentials.token_type + ' ' + tokens.access_token,
329 };
330 return { headers: this.addSharedMetadataHeaders(headers), res: r.res };
331 }
332 /**
333 * Generates an URL to revoke the given token.
334 * @param token The existing token to be revoked.
335 *
336 * @deprecated use instance method {@link OAuth2Client.getRevokeTokenURL}
337 */
338 static getRevokeTokenUrl(token) {
339 return new OAuth2Client().getRevokeTokenURL(token).toString();
340 }
341 /**
342 * Generates a URL to revoke the given token.
343 *
344 * @param token The existing token to be revoked.
345 */
346 getRevokeTokenURL(token) {
347 const url = new URL(this.endpoints.oauth2RevokeUrl);
348 url.searchParams.append('token', token);
349 return url;
350 }
351 revokeToken(token, callback) {
352 const opts = {
353 ...OAuth2Client.RETRY_CONFIG,
354 url: this.getRevokeTokenURL(token).toString(),
355 method: 'POST',
356 };
357 if (callback) {
358 this.transporter
359 .request(opts)
360 .then(r => callback(null, r), callback);
361 }
362 else {
363 return this.transporter.request(opts);
364 }
365 }
366 revokeCredentials(callback) {
367 if (callback) {
368 this.revokeCredentialsAsync().then(res => callback(null, res), callback);
369 }
370 else {
371 return this.revokeCredentialsAsync();
372 }
373 }
374 async revokeCredentialsAsync() {
375 const token = this.credentials.access_token;
376 this.credentials = {};
377 if (token) {
378 return this.revokeToken(token);
379 }
380 else {
381 throw new Error('No access token to revoke.');
382 }
383 }
384 request(opts, callback) {
385 if (callback) {
386 this.requestAsync(opts).then(r => callback(null, r), e => {
387 return callback(e, e.response);
388 });
389 }
390 else {
391 return this.requestAsync(opts);
392 }
393 }
394 async requestAsync(opts, reAuthRetried = false) {
395 let r2;
396 try {
397 const r = await this.getRequestMetadataAsync(opts.url);
398 opts.headers = opts.headers || {};
399 if (r.headers && r.headers['x-goog-user-project']) {
400 opts.headers['x-goog-user-project'] = r.headers['x-goog-user-project'];
401 }
402 if (r.headers && r.headers.Authorization) {
403 opts.headers.Authorization = r.headers.Authorization;
404 }
405 if (this.apiKey) {
406 opts.headers['X-Goog-Api-Key'] = this.apiKey;
407 }
408 r2 = await this.transporter.request(opts);
409 }
410 catch (e) {
411 const res = e.response;
412 if (res) {
413 const statusCode = res.status;
414 // Retry the request for metadata if the following criteria are true:
415 // - We haven't already retried. It only makes sense to retry once.
416 // - The response was a 401 or a 403
417 // - The request didn't send a readableStream
418 // - An access_token and refresh_token were available, but either no
419 // expiry_date was available or the forceRefreshOnFailure flag is set.
420 // The absent expiry_date case can happen when developers stash the
421 // access_token and refresh_token for later use, but the access_token
422 // fails on the first try because it's expired. Some developers may
423 // choose to enable forceRefreshOnFailure to mitigate time-related
424 // errors.
425 // Or the following criteria are true:
426 // - We haven't already retried. It only makes sense to retry once.
427 // - The response was a 401 or a 403
428 // - The request didn't send a readableStream
429 // - No refresh_token was available
430 // - An access_token and a refreshHandler callback were available, but
431 // either no expiry_date was available or the forceRefreshOnFailure
432 // flag is set. The access_token fails on the first try because it's
433 // expired. Some developers may choose to enable forceRefreshOnFailure
434 // to mitigate time-related errors.
435 const mayRequireRefresh = this.credentials &&
436 this.credentials.access_token &&
437 this.credentials.refresh_token &&
438 (!this.credentials.expiry_date || this.forceRefreshOnFailure);
439 const mayRequireRefreshWithNoRefreshToken = this.credentials &&
440 this.credentials.access_token &&
441 !this.credentials.refresh_token &&
442 (!this.credentials.expiry_date || this.forceRefreshOnFailure) &&
443 this.refreshHandler;
444 const isReadableStream = res.config.data instanceof stream.Readable;
445 const isAuthErr = statusCode === 401 || statusCode === 403;
446 if (!reAuthRetried &&
447 isAuthErr &&
448 !isReadableStream &&
449 mayRequireRefresh) {
450 await this.refreshAccessTokenAsync();
451 return this.requestAsync(opts, true);
452 }
453 else if (!reAuthRetried &&
454 isAuthErr &&
455 !isReadableStream &&
456 mayRequireRefreshWithNoRefreshToken) {
457 const refreshedAccessToken = await this.processAndValidateRefreshHandler();
458 if (refreshedAccessToken === null || refreshedAccessToken === void 0 ? void 0 : refreshedAccessToken.access_token) {
459 this.setCredentials(refreshedAccessToken);
460 }
461 return this.requestAsync(opts, true);
462 }
463 }
464 throw e;
465 }
466 return r2;
467 }
468 verifyIdToken(options, callback) {
469 // This function used to accept two arguments instead of an options object.
470 // Check the types to help users upgrade with less pain.
471 // This check can be removed after a 2.0 release.
472 if (callback && typeof callback !== 'function') {
473 throw new Error('This method accepts an options object as the first parameter, which includes the idToken, audience, and maxExpiry.');
474 }
475 if (callback) {
476 this.verifyIdTokenAsync(options).then(r => callback(null, r), callback);
477 }
478 else {
479 return this.verifyIdTokenAsync(options);
480 }
481 }
482 async verifyIdTokenAsync(options) {
483 if (!options.idToken) {
484 throw new Error('The verifyIdToken method requires an ID Token');
485 }
486 const response = await this.getFederatedSignonCertsAsync();
487 const login = await this.verifySignedJwtWithCertsAsync(options.idToken, response.certs, options.audience, this.issuers, options.maxExpiry);
488 return login;
489 }
490 /**
491 * Obtains information about the provisioned access token. Especially useful
492 * if you want to check the scopes that were provisioned to a given token.
493 *
494 * @param accessToken Required. The Access Token for which you want to get
495 * user info.
496 */
497 async getTokenInfo(accessToken) {
498 const { data } = await this.transporter.request({
499 ...OAuth2Client.RETRY_CONFIG,
500 method: 'POST',
501 headers: {
502 'Content-Type': 'application/x-www-form-urlencoded',
503 Authorization: `Bearer ${accessToken}`,
504 },
505 url: this.endpoints.tokenInfoUrl.toString(),
506 });
507 const info = Object.assign({
508 expiry_date: new Date().getTime() + data.expires_in * 1000,
509 scopes: data.scope.split(' '),
510 }, data);
511 delete info.expires_in;
512 delete info.scope;
513 return info;
514 }
515 getFederatedSignonCerts(callback) {
516 if (callback) {
517 this.getFederatedSignonCertsAsync().then(r => callback(null, r.certs, r.res), callback);
518 }
519 else {
520 return this.getFederatedSignonCertsAsync();
521 }
522 }
523 async getFederatedSignonCertsAsync() {
524 const nowTime = new Date().getTime();
525 const format = (0, crypto_1.hasBrowserCrypto)()
526 ? CertificateFormat.JWK
527 : CertificateFormat.PEM;
528 if (this.certificateExpiry &&
529 nowTime < this.certificateExpiry.getTime() &&
530 this.certificateCacheFormat === format) {
531 return { certs: this.certificateCache, format };
532 }
533 let res;
534 let url;
535 switch (format) {
536 case CertificateFormat.PEM:
537 url = this.endpoints.oauth2FederatedSignonPemCertsUrl.toString();
538 break;
539 case CertificateFormat.JWK:
540 url = this.endpoints.oauth2FederatedSignonJwkCertsUrl.toString();
541 break;
542 default:
543 throw new Error(`Unsupported certificate format ${format}`);
544 }
545 try {
546 res = await this.transporter.request({
547 ...OAuth2Client.RETRY_CONFIG,
548 url,
549 });
550 }
551 catch (e) {
552 if (e instanceof Error) {
553 e.message = `Failed to retrieve verification certificates: ${e.message}`;
554 }
555 throw e;
556 }
557 const cacheControl = res ? res.headers['cache-control'] : undefined;
558 let cacheAge = -1;
559 if (cacheControl) {
560 const pattern = new RegExp('max-age=([0-9]*)');
561 const regexResult = pattern.exec(cacheControl);
562 if (regexResult && regexResult.length === 2) {
563 // Cache results with max-age (in seconds)
564 cacheAge = Number(regexResult[1]) * 1000; // milliseconds
565 }
566 }
567 let certificates = {};
568 switch (format) {
569 case CertificateFormat.PEM:
570 certificates = res.data;
571 break;
572 case CertificateFormat.JWK:
573 for (const key of res.data.keys) {
574 certificates[key.kid] = key;
575 }
576 break;
577 default:
578 throw new Error(`Unsupported certificate format ${format}`);
579 }
580 const now = new Date();
581 this.certificateExpiry =
582 cacheAge === -1 ? null : new Date(now.getTime() + cacheAge);
583 this.certificateCache = certificates;
584 this.certificateCacheFormat = format;
585 return { certs: certificates, format, res };
586 }
587 getIapPublicKeys(callback) {
588 if (callback) {
589 this.getIapPublicKeysAsync().then(r => callback(null, r.pubkeys, r.res), callback);
590 }
591 else {
592 return this.getIapPublicKeysAsync();
593 }
594 }
595 async getIapPublicKeysAsync() {
596 let res;
597 const url = this.endpoints.oauth2IapPublicKeyUrl.toString();
598 try {
599 res = await this.transporter.request({
600 ...OAuth2Client.RETRY_CONFIG,
601 url,
602 });
603 }
604 catch (e) {
605 if (e instanceof Error) {
606 e.message = `Failed to retrieve verification certificates: ${e.message}`;
607 }
608 throw e;
609 }
610 return { pubkeys: res.data, res };
611 }
612 verifySignedJwtWithCerts() {
613 // To make the code compatible with browser SubtleCrypto we need to make
614 // this method async.
615 throw new Error('verifySignedJwtWithCerts is removed, please use verifySignedJwtWithCertsAsync instead.');
616 }
617 /**
618 * Verify the id token is signed with the correct certificate
619 * and is from the correct audience.
620 * @param jwt The jwt to verify (The ID Token in this case).
621 * @param certs The array of certs to test the jwt against.
622 * @param requiredAudience The audience to test the jwt against.
623 * @param issuers The allowed issuers of the jwt (Optional).
624 * @param maxExpiry The max expiry the certificate can be (Optional).
625 * @return Returns a promise resolving to LoginTicket on verification.
626 */
627 async verifySignedJwtWithCertsAsync(jwt, certs, requiredAudience, issuers, maxExpiry) {
628 const crypto = (0, crypto_1.createCrypto)();
629 if (!maxExpiry) {
630 maxExpiry = OAuth2Client.DEFAULT_MAX_TOKEN_LIFETIME_SECS_;
631 }
632 const segments = jwt.split('.');
633 if (segments.length !== 3) {
634 throw new Error('Wrong number of segments in token: ' + jwt);
635 }
636 const signed = segments[0] + '.' + segments[1];
637 let signature = segments[2];
638 let envelope;
639 let payload;
640 try {
641 envelope = JSON.parse(crypto.decodeBase64StringUtf8(segments[0]));
642 }
643 catch (err) {
644 if (err instanceof Error) {
645 err.message = `Can't parse token envelope: ${segments[0]}': ${err.message}`;
646 }
647 throw err;
648 }
649 if (!envelope) {
650 throw new Error("Can't parse token envelope: " + segments[0]);
651 }
652 try {
653 payload = JSON.parse(crypto.decodeBase64StringUtf8(segments[1]));
654 }
655 catch (err) {
656 if (err instanceof Error) {
657 err.message = `Can't parse token payload '${segments[0]}`;
658 }
659 throw err;
660 }
661 if (!payload) {
662 throw new Error("Can't parse token payload: " + segments[1]);
663 }
664 if (!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) {
665 // If this is not present, then there's no reason to attempt verification
666 throw new Error('No pem found for envelope: ' + JSON.stringify(envelope));
667 }
668 const cert = certs[envelope.kid];
669 if (envelope.alg === 'ES256') {
670 signature = formatEcdsa.joseToDer(signature, 'ES256').toString('base64');
671 }
672 const verified = await crypto.verify(cert, signed, signature);
673 if (!verified) {
674 throw new Error('Invalid token signature: ' + jwt);
675 }
676 if (!payload.iat) {
677 throw new Error('No issue time in token: ' + JSON.stringify(payload));
678 }
679 if (!payload.exp) {
680 throw new Error('No expiration time in token: ' + JSON.stringify(payload));
681 }
682 const iat = Number(payload.iat);
683 if (isNaN(iat))
684 throw new Error('iat field using invalid format');
685 const exp = Number(payload.exp);
686 if (isNaN(exp))
687 throw new Error('exp field using invalid format');
688 const now = new Date().getTime() / 1000;
689 if (exp >= now + maxExpiry) {
690 throw new Error('Expiration time too far in future: ' + JSON.stringify(payload));
691 }
692 const earliest = iat - OAuth2Client.CLOCK_SKEW_SECS_;
693 const latest = exp + OAuth2Client.CLOCK_SKEW_SECS_;
694 if (now < earliest) {
695 throw new Error('Token used too early, ' +
696 now +
697 ' < ' +
698 earliest +
699 ': ' +
700 JSON.stringify(payload));
701 }
702 if (now > latest) {
703 throw new Error('Token used too late, ' +
704 now +
705 ' > ' +
706 latest +
707 ': ' +
708 JSON.stringify(payload));
709 }
710 if (issuers && issuers.indexOf(payload.iss) < 0) {
711 throw new Error('Invalid issuer, expected one of [' +
712 issuers +
713 '], but got ' +
714 payload.iss);
715 }
716 // Check the audience matches if we have one
717 if (typeof requiredAudience !== 'undefined' && requiredAudience !== null) {
718 const aud = payload.aud;
719 let audVerified = false;
720 // If the requiredAudience is an array, check if it contains token
721 // audience
722 if (requiredAudience.constructor === Array) {
723 audVerified = requiredAudience.indexOf(aud) > -1;
724 }
725 else {
726 audVerified = aud === requiredAudience;
727 }
728 if (!audVerified) {
729 throw new Error('Wrong recipient, payload audience != requiredAudience');
730 }
731 }
732 return new loginticket_1.LoginTicket(envelope, payload);
733 }
734 /**
735 * Returns a promise that resolves with AccessTokenResponse type if
736 * refreshHandler is defined.
737 * If not, nothing is returned.
738 */
739 async processAndValidateRefreshHandler() {
740 if (this.refreshHandler) {
741 const accessTokenResponse = await this.refreshHandler();
742 if (!accessTokenResponse.access_token) {
743 throw new Error('No access token is returned by the refreshHandler callback.');
744 }
745 return accessTokenResponse;
746 }
747 return;
748 }
749 /**
750 * Returns true if a token is expired or will expire within
751 * eagerRefreshThresholdMillismilliseconds.
752 * If there is no expiry time, assumes the token is not expired or expiring.
753 */
754 isTokenExpiring() {
755 const expiryDate = this.credentials.expiry_date;
756 return expiryDate
757 ? expiryDate <= new Date().getTime() + this.eagerRefreshThresholdMillis
758 : false;
759 }
760}
761exports.OAuth2Client = OAuth2Client;
762/**
763 * @deprecated use instance's {@link OAuth2Client.endpoints}
764 */
765OAuth2Client.GOOGLE_TOKEN_INFO_URL = 'https://oauth2.googleapis.com/tokeninfo';
766/**
767 * Clock skew - five minutes in seconds
768 */
769OAuth2Client.CLOCK_SKEW_SECS_ = 300;
770/**
771 * The default max Token Lifetime is one day in seconds
772 */
773OAuth2Client.DEFAULT_MAX_TOKEN_LIFETIME_SECS_ = 86400;