oauth2client.js
22.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
"use strict";
/**
* Copyright 2012 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
var querystring = require("querystring");
var pemverifier_1 = require("./../pemverifier");
var transporters_1 = require("./../transporters");
var authclient_1 = require("./authclient");
var loginticket_1 = require("./loginticket");
var merge = require('lodash.merge');
var noop = Function.prototype;
var OAuth2Client = /** @class */ (function (_super) {
__extends(OAuth2Client, _super);
/**
* Handles OAuth2 flow for Google APIs.
*
* @param {string=} clientId The authentication client ID.
* @param {string=} clientSecret The authentication client secret.
* @param {string=} redirectUri The URI to redirect to after completing the auth request.
* @param {Object=} opt_opts optional options for overriding the given parameters.
* @constructor
*/
function OAuth2Client(clientId, clientSecret, redirectUri, opt_opts) {
var _this = _super.call(this) || this;
_this._certificateCache = null;
_this._certificateExpiry = null;
_this._clientId = clientId;
_this._clientSecret = clientSecret;
_this._redirectUri = redirectUri;
_this._opts = opt_opts || {};
_this.credentials = {};
return _this;
}
/**
* Generates URL for consent page landing.
* @param {object=} opt_opts Options.
* @return {string} URL to consent page.
*/
OAuth2Client.prototype.generateAuthUrl = function (opt_opts) {
var opts = opt_opts || {};
opts.response_type = opts.response_type || 'code';
opts.client_id = opts.client_id || this._clientId;
opts.redirect_uri = opts.redirect_uri || this._redirectUri;
// Allow scopes to be passed either as array or a string
if (opts.scope instanceof Array) {
opts.scope = opts.scope.join(' ');
}
var rootUrl = this._opts.authBaseUrl || OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_;
return rootUrl + '?' + querystring.stringify(opts);
};
/**
* Gets the access token for the given code.
* @param {string} code The authorization code.
* @param {function=} callback Optional callback fn.
*/
OAuth2Client.prototype.getToken = function (code, callback) {
var uri = this._opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
var values = {
code: code,
client_id: this._clientId,
client_secret: this._clientSecret,
redirect_uri: this._redirectUri,
grant_type: 'authorization_code'
};
this.transporter.request({ method: 'POST', uri: uri, form: values, json: true }, function (err, tokens, response) {
if (!err && tokens && tokens.expires_in) {
tokens.expiry_date =
((new Date()).getTime() + (tokens.expires_in * 1000));
delete tokens.expires_in;
}
var done = callback || noop;
done(err, tokens, response);
});
};
/**
* Refreshes the access token.
* @param {string} refresh_token Existing refresh token.
* @param {function=} callback Optional callback.
* @private
*/
OAuth2Client.prototype.refreshToken = function (refresh_token, callback) {
var uri = this._opts.tokenUrl || OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_;
var values = {
refresh_token: refresh_token,
client_id: this._clientId,
client_secret: this._clientSecret,
grant_type: 'refresh_token'
};
// request for new token
return this.transporter.request({ method: 'POST', uri: uri, form: values, json: true }, function (err, tokens, response) {
if (!err && tokens && tokens.expires_in) {
tokens.expiry_date =
((new Date()).getTime() + (tokens.expires_in * 1000));
delete tokens.expires_in;
}
var done = callback || noop;
done(err, tokens, response);
});
};
/**
* Retrieves the access token using refresh token
*
* @deprecated use getRequestMetadata instead.
* @param {function} callback callback
*/
OAuth2Client.prototype.refreshAccessToken = function (callback) {
var _this = this;
if (!this.credentials.refresh_token) {
callback(new Error('No refresh token is set.'), null);
return;
}
this.refreshToken(this.credentials.refresh_token, function (err, result, response) {
if (err) {
callback(err, null, response);
}
else {
var tokens = result;
tokens.refresh_token = _this.credentials.refresh_token;
_this.credentials = tokens;
callback(null, _this.credentials, response);
}
});
};
/**
* Get a non-expired access token, after refreshing if necessary
*
* @param {function} callback Callback to call with the access token
*/
OAuth2Client.prototype.getAccessToken = function (callback) {
var expiryDate = this.credentials.expiry_date;
// if no expiry time, assume it's not expired
var isTokenExpired = expiryDate ? expiryDate <= (new Date()).getTime() : false;
if (!this.credentials.access_token && !this.credentials.refresh_token) {
return callback(new Error('No access or refresh token is set.'), null);
}
var shouldRefresh = !this.credentials.access_token || isTokenExpired;
if (shouldRefresh && this.credentials.refresh_token) {
if (!this.credentials.refresh_token) {
return callback(new Error('No refresh token is set.'), null);
}
this.refreshAccessToken(function (err, tokens, response) {
if (err) {
return callback(err, null, response);
}
if (!tokens || (tokens && !tokens.access_token)) {
return callback(new Error('Could not refresh access token.'), null, response);
}
return callback(null, tokens.access_token, response);
});
}
else {
return callback(null, this.credentials.access_token, null);
}
};
/**
* getRequestMetadata obtains auth metadata to be used by requests.
*
* getRequestMetadata is the main authentication interface. It takes an
* optional uri which when present is the endpoint being accessed, and a
* callback func(err, metadata_obj, response) where metadata_obj contains
* authorization metadata fields and response is an optional response object.
*
* In OAuth2Client, metadata_obj has the form.
*
* {Authorization: 'Bearer <access_token_value>'}
*
* @param {string} opt_uri the Uri being authorized
* @param {function} metadataCb the func described above
*/
OAuth2Client.prototype.getRequestMetadata = function (opt_uri, metadataCb) {
var _this = this;
var thisCreds = this.credentials;
if (!thisCreds.access_token && !thisCreds.refresh_token && !this.apiKey) {
return metadataCb(new Error('No access, refresh token or API key is set.'), null);
}
// if no expiry time, assume it's not expired
var expiryDate = thisCreds.expiry_date;
var isTokenExpired = expiryDate ? expiryDate <= (new Date()).getTime() : false;
if (thisCreds.access_token && !isTokenExpired) {
thisCreds.token_type = thisCreds.token_type || 'Bearer';
var headers = {
Authorization: thisCreds.token_type + ' ' + thisCreds.access_token
};
return metadataCb(null, headers, null);
}
if (this.apiKey) {
return metadataCb(null, {}, null);
}
return this.refreshToken(thisCreds.refresh_token, function (err, tokens, response) {
// If the error code is 403 or 404, go to the else so the error
// message is replaced. Otherwise, return the error.
if (err && err.code != 403 &&
err.code != 404) {
return metadataCb(err, null, response);
}
else {
if (!tokens || (tokens && !tokens.access_token)) {
return metadataCb(new Error('Could not refresh access token.'), null, response);
}
var credentials = _this.credentials;
credentials.token_type = credentials.token_type || 'Bearer';
tokens.refresh_token = credentials.refresh_token;
_this.credentials = tokens;
var headers = {
Authorization: credentials.token_type + ' ' + tokens.access_token
};
return metadataCb(err, headers, response);
}
});
};
/**
* Revokes the access given to token.
* @param {string} token The existing token to be revoked.
* @param {function=} callback Optional callback fn.
*/
OAuth2Client.prototype.revokeToken = function (token, callback) {
this.transporter.request({
uri: OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_ + '?' +
querystring.stringify({ token: token }),
json: true
}, callback);
};
/**
* Revokes access token and clears the credentials object
* @param {Function=} callback callback
*/
OAuth2Client.prototype.revokeCredentials = function (callback) {
var token = this.credentials.access_token;
this.credentials = {};
if (token) {
this.revokeToken(token, callback);
}
else {
callback(new transporters_1.RequestError('No access token to revoke.'), null, null);
}
};
/**
* Provides a request implementation with OAuth 2.0 flow.
* If credentials have a refresh_token, in cases of HTTP
* 401 and 403 responses, it automatically asks for a new
* access token and replays the unsuccessful request.
* @param {object} opts Request options.
* @param {function} callback callback.
* @return {Request} Request object
*/
OAuth2Client.prototype.request = function (opts, callback) {
/* jshint latedef:false */
var _this = this;
// Callbacks will close over this to ensure that we only retry once
var retry = true;
var unusedUri = null;
// Declare authCb upfront to avoid the linter complaining about use before
// declaration.
var authCb;
// Hook the callback routine to call the _postRequest method.
var postRequestCb = function (err, body, resp) {
var statusCode = resp && resp.statusCode;
// Automatically retry 401 and 403 responses
// if err is set and is unrelated to response
// then getting credentials failed, and retrying won't help
if (retry && (statusCode === 401 || statusCode === 403) &&
(!err || err.code === statusCode)) {
/* It only makes sense to retry once, because the retry is intended
* to handle expiration-related failures. If refreshing the token
* does not fix the failure, then refreshing again probably won't
* help */
retry = false;
// Force token refresh
_this.refreshAccessToken(function () {
_this.getRequestMetadata(unusedUri, authCb);
});
}
else {
_this.postRequest(err, body, resp, callback);
}
};
authCb = function (err, headers, response) {
if (err) {
postRequestCb(err, null, response);
}
else {
if (headers) {
opts.headers = opts.headers || {};
opts.headers.Authorization = headers.Authorization;
}
if (_this.apiKey) {
if (opts.qs) {
opts.qs = merge({}, opts.qs, { key: _this.apiKey });
}
else {
opts.qs = { key: _this.apiKey };
}
}
return _this._makeRequest(opts, postRequestCb);
}
};
return this.getRequestMetadata(unusedUri, authCb);
};
/**
* Makes a request without paying attention to refreshing or anything
* Assumes that all credentials are set correctly.
* @param {object} opts Options for request
* @param {Function} callback callback function
* @return {Request} The request object created
*/
OAuth2Client.prototype._makeRequest = function (opts, callback) {
return this.transporter.request(opts, callback);
};
/**
* Allows inheriting classes to inspect and alter the request result.
* @param {object} err Error result.
* @param {object} result The result.
* @param {object} result The HTTP response.
* @param {Function} callback The callback.
* @private
*/
OAuth2Client.prototype.postRequest = function (err, result, response, callback) {
callback(err, result, response);
};
/**
* Verify id token is token by checking the certs and audience
* @param {string} idToken ID Token.
* @param {(string|Array.<string>)} audience The audience to verify against the ID Token
* @param {function=} callback Callback supplying GoogleLogin if successful
*/
OAuth2Client.prototype.verifyIdToken = function (idToken, audience, callback) {
var _this = this;
if (!idToken || !callback) {
throw new Error('The verifyIdToken method requires both ' +
'an ID Token and a callback method');
}
this.getFederatedSignonCerts((function (err, certs) {
if (err) {
callback(err, null);
}
var login;
try {
login = _this.verifySignedJwtWithCerts(idToken, certs, audience, OAuth2Client.ISSUERS_);
}
catch (err) {
callback(err);
return;
}
callback(null, login);
}).bind(this));
};
/**
* Gets federated sign-on certificates to use for verifying identity tokens.
* Returns certs as array structure, where keys are key ids, and values
* are PEM encoded certificates.
* @param {function=} callback Callback supplying the certificates
*/
OAuth2Client.prototype.getFederatedSignonCerts = function (callback) {
var _this = this;
var nowTime = (new Date()).getTime();
if (this._certificateExpiry &&
(nowTime < this._certificateExpiry.getTime())) {
callback(null, this._certificateCache);
return;
}
this.transporter.request({
method: 'GET',
uri: OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_CERTS_URL_,
json: true
}, function (err, body, response) {
if (err) {
callback(new transporters_1.RequestError('Failed to retrieve verification certificates: ' + err), null, response);
return;
}
var cacheControl = response.headers['cache-control'];
var cacheAge = -1;
if (cacheControl) {
var pattern = new RegExp('max-age=([0-9]*)');
var regexResult = pattern.exec(cacheControl);
if (regexResult.length === 2) {
// Cache results with max-age (in seconds)
cacheAge = Number(regexResult[1]) * 1000; // milliseconds
}
}
var now = new Date();
_this._certificateExpiry =
cacheAge === -1 ? null : new Date(now.getTime() + cacheAge);
_this._certificateCache = body;
callback(null, body, response);
});
};
/**
* Verify the id token is signed with the correct certificate
* and is from the correct audience.
* @param {string} jwt The jwt to verify (The ID Token in this case).
* @param {array} certs The array of certs to test the jwt against.
* @param {(string|Array.<string>)} requiredAudience The audience to test the jwt against.
* @param {array} issuers The allowed issuers of the jwt (Optional).
* @param {string} maxExpiry The max expiry the certificate can be (Optional).
* @return {LoginTicket} Returns a LoginTicket on verification.
*/
OAuth2Client.prototype.verifySignedJwtWithCerts = function (jwt, certs, requiredAudience, issuers, maxExpiry) {
if (!maxExpiry) {
maxExpiry = OAuth2Client.MAX_TOKEN_LIFETIME_SECS_;
}
var segments = jwt.split('.');
if (segments.length !== 3) {
throw new Error('Wrong number of segments in token: ' + jwt);
}
var signed = segments[0] + '.' + segments[1];
var signature = segments[2];
var envelope;
var payload;
try {
envelope = JSON.parse(this.decodeBase64(segments[0]));
}
catch (err) {
throw new Error('Can\'t parse token envelope: ' + segments[0]);
}
if (!envelope) {
throw new Error('Can\'t parse token envelope: ' + segments[0]);
}
try {
payload = JSON.parse(this.decodeBase64(segments[1]));
}
catch (err) {
throw new Error('Can\'t parse token payload: ' + segments[0]);
}
if (!payload) {
throw new Error('Can\'t parse token payload: ' + segments[1]);
}
if (!certs.hasOwnProperty(envelope.kid)) {
// If this is not present, then there's no reason to attempt verification
throw new Error('No pem found for envelope: ' + JSON.stringify(envelope));
}
var pem = certs[envelope.kid];
var pemVerifier = new pemverifier_1.PemVerifier();
var verified = pemVerifier.verify(pem, signed, signature, 'base64');
if (!verified) {
throw new Error('Invalid token signature: ' + jwt);
}
if (!payload.iat) {
throw new Error('No issue time in token: ' + JSON.stringify(payload));
}
if (!payload.exp) {
throw new Error('No expiration time in token: ' + JSON.stringify(payload));
}
var iat = parseInt(payload.iat, 10);
var exp = parseInt(payload.exp, 10);
var now = new Date().getTime() / 1000;
if (exp >= now + maxExpiry) {
throw new Error('Expiration time too far in future: ' + JSON.stringify(payload));
}
var earliest = iat - OAuth2Client.CLOCK_SKEW_SECS_;
var latest = exp + OAuth2Client.CLOCK_SKEW_SECS_;
if (now < earliest) {
throw new Error('Token used too early, ' + now + ' < ' + earliest + ': ' +
JSON.stringify(payload));
}
if (now > latest) {
throw new Error('Token used too late, ' + now + ' > ' + latest + ': ' +
JSON.stringify(payload));
}
if (issuers && issuers.indexOf(payload.iss) < 0) {
throw new Error('Invalid issuer, expected one of [' + issuers + '], but got ' +
payload.iss);
}
// Check the audience matches if we have one
if (typeof requiredAudience !== 'undefined' && requiredAudience !== null) {
var aud = payload.aud;
var audVerified = false;
// If the requiredAudience is an array, check if it contains token
// audience
if (requiredAudience.constructor === Array) {
audVerified = (requiredAudience.indexOf(aud) > -1);
}
else {
audVerified = (aud === requiredAudience);
}
if (!audVerified) {
throw new Error('Wrong recipient, payload audience != requiredAudience');
}
}
return new loginticket_1.LoginTicket(envelope, payload);
};
/**
* This is a utils method to decode a base64 string
* @param {string} b64String The string to base64 decode
* @return {string} The decoded string
*/
OAuth2Client.prototype.decodeBase64 = function (b64String) {
var buffer = new Buffer(b64String, 'base64');
return buffer.toString('utf8');
};
/**
* The base URL for auth endpoints.
*/
OAuth2Client.GOOGLE_OAUTH2_AUTH_BASE_URL_ = 'https://accounts.google.com/o/oauth2/auth';
/**
* The base endpoint for token retrieval.
*/
OAuth2Client.GOOGLE_OAUTH2_TOKEN_URL_ = 'https://accounts.google.com/o/oauth2/token';
/**
* The base endpoint to revoke tokens.
*/
OAuth2Client.GOOGLE_OAUTH2_REVOKE_URL_ = 'https://accounts.google.com/o/oauth2/revoke';
/**
* Google Sign on certificates.
*/
OAuth2Client.GOOGLE_OAUTH2_FEDERATED_SIGNON_CERTS_URL_ = 'https://www.googleapis.com/oauth2/v1/certs';
/**
* Clock skew - five minutes in seconds
*/
OAuth2Client.CLOCK_SKEW_SECS_ = 300;
/**
* Max Token Lifetime is one day in seconds
*/
OAuth2Client.MAX_TOKEN_LIFETIME_SECS_ = 86400;
/**
* The allowed oauth token issuers.
*/
OAuth2Client.ISSUERS_ = ['accounts.google.com', 'https://accounts.google.com'];
return OAuth2Client;
}(authclient_1.AuthClient));
exports.OAuth2Client = OAuth2Client;
//# sourceMappingURL=oauth2client.js.map