@@ -30,6 +30,7 @@ var Service_ = function(serviceName) {
30
30
this . tokenFormat_ = TOKEN_FORMAT . JSON ;
31
31
this . tokenHeaders_ = null ;
32
32
this . projectKey_ = eval ( 'Script' + 'App' ) . getProjectKey ( ) ;
33
+ this . expirationMinutes_ = 60 ;
33
34
} ;
34
35
35
36
/**
@@ -72,7 +73,7 @@ Service_.prototype.setTokenFormat = function(tokenFormat) {
72
73
} ;
73
74
74
75
/**
75
- * Sets the additional HTTP headers that should be sent when retrieving or
76
+ * Sets the additional HTTP headers that should be sent when retrieving or
76
77
* refreshing the access token.
77
78
* @param Object.<string,string> tokenHeaders A map of header names to values.
78
79
* @return {Service_ } This service, for chaining.
@@ -185,6 +186,48 @@ Service_.prototype.setParam = function(name, value) {
185
186
return this ;
186
187
} ;
187
188
189
+ /**
190
+ * Sets the private key to use for Service Account authorization.
191
+ * @param {string } privateKey The private key.
192
+ * @return {Service_ } This service, for chaining.
193
+ */
194
+ Service_ . prototype . setPrivateKey = function ( privateKey ) {
195
+ this . privateKey_ = privateKey ;
196
+ return this ;
197
+ } ;
198
+
199
+ /**
200
+ * Sets the issuer (iss) value to use for Service Account authorization.
201
+ * If not set the client ID will be used instead.
202
+ * @param {string } issuer This issuer value
203
+ * @return {Service_ } This service, for chaining.
204
+ */
205
+ Service_ . prototype . setIssuer = function ( issuer ) {
206
+ this . issuer_ = issuer ;
207
+ return this ;
208
+ } ;
209
+
210
+ /**
211
+ * Sets the subject (sub) value to use for Service Account authorization.
212
+ * @param {string } subject This subject value
213
+ * @return {Service_ } This service, for chaining.
214
+ */
215
+ Service_ . prototype . setSubject = function ( subject ) {
216
+ this . subject_ = subject ;
217
+ return this ;
218
+ } ;
219
+
220
+ /**
221
+ * Sets number of minutes that a token obtained through Service Account authorization should be valid.
222
+ * Default: 60 minutes.
223
+ * @param {string } expirationMinutes The expiration duration in minutes.
224
+ * @return {Service_ } This service, for chaining.
225
+ */
226
+ Service_ . prototype . setExpirationMinutes = function ( expirationMinutes ) {
227
+ this . expirationMinutes_ = expirationMinutes ;
228
+ return this ;
229
+ } ;
230
+
188
231
/**
189
232
* Gets the authorization URL. The first step in getting an OAuth2 token is to
190
233
* have the user visit this URL and approve the authorization request. The
@@ -273,23 +316,23 @@ Service_.prototype.handleCallback = function(callbackRequest) {
273
316
*/
274
317
Service_ . prototype . hasAccess = function ( ) {
275
318
var token = this . getToken_ ( ) ;
276
- if ( ! token ) {
277
- return false ;
278
- }
279
- var expires_in = token . expires_in || token . expires ;
280
- if ( expires_in ) {
281
- var expires_time = token . granted_time + expires_in ;
282
- var now = getTimeInSeconds_ ( new Date ( ) ) ;
283
- if ( expires_time - now < Service_ . EXPIRATION_BUFFER_SECONDS_ ) {
284
- if ( token . refresh_token ) {
285
- try {
286
- this . refresh ( ) ;
287
- } catch ( e ) {
288
- return false ;
289
- }
290
- } else {
319
+ if ( ! token || this . isExpired_ ( token ) ) {
320
+ if ( token && token . refresh_token ) {
321
+ try {
322
+ this . refresh ( ) ;
323
+ } catch ( e ) {
324
+ this . lastError_ = e ;
291
325
return false ;
292
326
}
327
+ } else if ( this . privateKey_ ) {
328
+ try {
329
+ this . exchangeJwt_ ( ) ;
330
+ } catch ( e ) {
331
+ this . lastError_ = e ;
332
+ return false ;
333
+ }
334
+ } else {
335
+ return false ;
293
336
}
294
337
}
295
338
return true ;
@@ -316,7 +359,16 @@ Service_.prototype.reset = function() {
316
359
validate_ ( {
317
360
'Property store' : this . propertyStore_
318
361
} ) ;
319
- this . propertyStore_ . deleteProperty ( this . getPropertyKey ( this . serviceName_ ) ) ;
362
+ this . propertyStore_ . deleteProperty ( this . getPropertyKey_ ( this . serviceName_ ) ) ;
363
+ } ;
364
+
365
+ /**
366
+ * Gets the last error that occurred this execution when trying to automatically refresh
367
+ * or generate an access token.
368
+ * @return {Exception } An error, if any.
369
+ */
370
+ Service_ . prototype . getLastError = function ( ) {
371
+ return this . lastError_ ;
320
372
} ;
321
373
322
374
/**
@@ -397,7 +449,7 @@ Service_.prototype.saveToken_ = function(token) {
397
449
validate_ ( {
398
450
'Property store' : this . propertyStore_
399
451
} ) ;
400
- var key = this . getPropertyKey ( this . serviceName_ ) ;
452
+ var key = this . getPropertyKey_ ( this . serviceName_ ) ;
401
453
var value = JSON . stringify ( token ) ;
402
454
this . propertyStore_ . setProperty ( key , value ) ;
403
455
if ( this . cache_ ) {
@@ -414,7 +466,7 @@ Service_.prototype.getToken_ = function() {
414
466
validate_ ( {
415
467
'Property store' : this . propertyStore_
416
468
} ) ;
417
- var key = this . getPropertyKey ( this . serviceName_ ) ;
469
+ var key = this . getPropertyKey_ ( this . serviceName_ ) ;
418
470
var token ;
419
471
if ( this . cache_ ) {
420
472
token = this . cache_ . get ( key ) ;
@@ -438,6 +490,91 @@ Service_.prototype.getToken_ = function() {
438
490
* @return {string } The property key.
439
491
* @private
440
492
*/
441
- Service_ . prototype . getPropertyKey = function ( serviceName ) {
493
+ Service_ . prototype . getPropertyKey_ = function ( serviceName ) {
442
494
return 'oauth2.' + serviceName ;
443
495
} ;
496
+
497
+ /**
498
+ * Determines if a retrieved token is still valid.
499
+ * @param {Object } token The token to validate.
500
+ * @return {boolean } True if it has expired, false otherwise.
501
+ * @private
502
+ */
503
+ Service_ . prototype . isExpired_ = function ( token ) {
504
+ var expires_in = token . expires_in || token . expires ;
505
+ if ( ! expires_in ) {
506
+ return false ;
507
+ } else {
508
+ var expires_time = token . granted_time + expires_in ;
509
+ var now = getTimeInSeconds_ ( new Date ( ) ) ;
510
+ return expires_time - now < Service_ . EXPIRATION_BUFFER_SECONDS_ ;
511
+ }
512
+ } ;
513
+
514
+ /**
515
+ * Uses the service account flow to exchange a signed JSON Web Token (JWT) for an
516
+ * access token.
517
+ */
518
+ Service_ . prototype . exchangeJwt_ = function ( ) {
519
+ validate_ ( {
520
+ 'Token URL' : this . tokenUrl_
521
+ } ) ;
522
+ var jwt = this . createJwt_ ( ) ;
523
+ var headers = {
524
+ 'Accept' : this . tokenFormat_
525
+ } ;
526
+ if ( this . tokenHeaders_ ) {
527
+ headers = _ . extend ( headers , this . tokenHeaders_ ) ;
528
+ }
529
+ var response = UrlFetchApp . fetch ( this . tokenUrl_ , {
530
+ method : 'post' ,
531
+ headers : headers ,
532
+ payload : {
533
+ assertion : jwt ,
534
+ grant_type : 'urn:ietf:params:oauth:grant-type:jwt-bearer'
535
+ } ,
536
+ muteHttpExceptions : true
537
+ } ) ;
538
+ var token = this . parseToken_ ( response . getContentText ( ) ) ;
539
+ if ( response . getResponseCode ( ) != 200 ) {
540
+ var reason = token . error ? token . error : response . getResponseCode ( ) ;
541
+ throw 'Error retrieving token: ' + reason ;
542
+ }
543
+ this . saveToken_ ( token ) ;
544
+ } ;
545
+
546
+ /**
547
+ * Creates a signed JSON Web Token (JWT) for use with Service Account authorization.
548
+ * @return {string } The signed JWT.
549
+ * @private
550
+ */
551
+ Service_ . prototype . createJwt_ = function ( ) {
552
+ validate_ ( {
553
+ 'Private key' : this . privateKey_ ,
554
+ 'Token URL' : this . tokenUrl_ ,
555
+ 'Issuer or Client ID' : this . issuer_ || this . clientId_
556
+ } ) ;
557
+ var header = {
558
+ alg : 'RS256' ,
559
+ typ : 'JWT'
560
+ } ;
561
+ var now = new Date ( ) ;
562
+ var expires = new Date ( now . getTime ( ) ) ;
563
+ expires . setMinutes ( expires . getMinutes ( ) + this . expirationMinutes_ ) ;
564
+ var claimSet = {
565
+ iss : this . issuer_ || this . clientId_ ,
566
+ aud : this . tokenUrl_ ,
567
+ exp : Math . round ( expires . getTime ( ) / 1000 ) ,
568
+ iat : Math . round ( now . getTime ( ) / 1000 )
569
+ } ;
570
+ if ( this . subject_ ) {
571
+ claimSet [ 'sub' ] = this . subject_ ;
572
+ }
573
+ if ( this . params_ [ 'scope' ] ) {
574
+ claimSet [ 'scope' ] = this . params_ [ 'scope' ] ;
575
+ }
576
+ var toSign = Utilities . base64EncodeWebSafe ( JSON . stringify ( header ) ) + '.' + Utilities . base64EncodeWebSafe ( JSON . stringify ( claimSet ) ) ;
577
+ var signatureBytes = Utilities . computeRsaSha256Signature ( toSign , this . privateKey_ ) ;
578
+ var signature = Utilities . base64EncodeWebSafe ( signatureBytes ) ;
579
+ return toSign + '.' + signature ;
580
+ } ;
0 commit comments