@@ -128,6 +128,13 @@ var Service_ = function(serviceName) {
128
128
*/
129
129
Service_ . EXPIRATION_BUFFER_SECONDS_ = 60 ;
130
130
131
+ /**
132
+ * The number of milliseconds that a token should remain in the cache.
133
+ * @type {number }
134
+ * @private
135
+ */
136
+ Service_ . LOCK_EXPIRATION_MILLISECONDS_ = 30 * 1000 ;
137
+
131
138
/**
132
139
* Sets the service's authorization base URL (required). For Google services
133
140
* this URL should be
@@ -257,6 +264,7 @@ Service_.prototype.setClientSecret = function(clientSecret) {
257
264
* @param {PropertiesService.Properties } propertyStore The property store to use
258
265
* when persisting credentials.
259
266
* @return {Service_ } This service, for chaining.
267
+ * @see https://developers.google.com/apps-script/reference/properties/
260
268
*/
261
269
Service_ . prototype . setPropertyStore = function ( propertyStore ) {
262
270
this . propertyStore_ = propertyStore ;
@@ -271,12 +279,27 @@ Service_.prototype.setPropertyStore = function(propertyStore) {
271
279
* @param {CacheService.Cache } cache The cache to use when persisting
272
280
* credentials.
273
281
* @return {Service_ } This service, for chaining.
282
+ * @see https://developers.google.com/apps-script/reference/cache/
274
283
*/
275
284
Service_ . prototype . setCache = function ( cache ) {
276
285
this . cache_ = cache ;
277
286
return this ;
278
287
} ;
279
288
289
+ /**
290
+ * Sets the lock to use when checking and refreshing credentials (optional).
291
+ * Using a lock will ensure that only one execution will be able to access the
292
+ * stored credentials at a time. This can prevent race conditions that arise
293
+ * when two executions attempt to refresh an expired token.
294
+ * @param {LockService.Lock } lock The lock to use when accessing credentials.
295
+ * @return {Service_ } This service, for chaining.
296
+ * @see https://developers.google.com/apps-script/reference/lock/
297
+ */
298
+ Service_ . prototype . setLock = function ( lock ) {
299
+ this . lock_ = lock ;
300
+ return this ;
301
+ } ;
302
+
280
303
/**
281
304
* Sets the scope or scopes to request during the authorization flow (optional).
282
305
* If the scope value is an array it will be joined using the separator before
@@ -437,27 +460,29 @@ Service_.prototype.handleCallback = function(callbackRequest) {
437
460
* otherwise.
438
461
*/
439
462
Service_ . prototype . hasAccess = function ( ) {
440
- var token = this . getToken ( ) ;
441
- if ( ! token || this . isExpired_ ( token ) ) {
442
- if ( token && token . refresh_token ) {
443
- try {
444
- this . refresh ( ) ;
445
- } catch ( e ) {
446
- this . lastError_ = e ;
447
- return false ;
448
- }
449
- } else if ( this . privateKey_ ) {
450
- try {
451
- this . exchangeJwt_ ( ) ;
452
- } catch ( e ) {
453
- this . lastError_ = e ;
463
+ return this . lockable_ ( function ( ) {
464
+ var token = this . getToken ( ) ;
465
+ if ( ! token || this . isExpired_ ( token ) ) {
466
+ if ( token && token . refresh_token ) {
467
+ try {
468
+ this . refresh ( ) ;
469
+ } catch ( e ) {
470
+ this . lastError_ = e ;
471
+ return false ;
472
+ }
473
+ } else if ( this . privateKey_ ) {
474
+ try {
475
+ this . exchangeJwt_ ( ) ;
476
+ } catch ( e ) {
477
+ this . lastError_ = e ;
478
+ return false ;
479
+ }
480
+ } else {
454
481
return false ;
455
482
}
456
- } else {
457
- return false ;
458
483
}
459
- }
460
- return true ;
484
+ return true ;
485
+ } ) ;
461
486
} ;
462
487
463
488
/**
@@ -479,8 +504,7 @@ Service_.prototype.getAccessToken = function() {
479
504
* re-authorized.
480
505
*/
481
506
Service_ . prototype . reset = function ( ) {
482
- var storage = this . getStorage ( ) ;
483
- storage . removeValue ( null ) ;
507
+ this . getStorage ( ) . removeValue ( null ) ;
484
508
} ;
485
509
486
510
/**
@@ -565,38 +589,41 @@ Service_.prototype.refresh = function() {
565
589
'Client Secret' : this . clientSecret_ ,
566
590
'Token URL' : this . tokenUrl_
567
591
} ) ;
568
- var token = this . getToken ( ) ;
569
- if ( ! token . refresh_token ) {
570
- throw new Error ( 'Offline access is required.' ) ;
571
- }
572
- var headers = {
573
- 'Accept' : this . tokenFormat_
574
- } ;
575
- if ( this . tokenHeaders_ ) {
576
- headers = extend_ ( headers , this . tokenHeaders_ ) ;
577
- }
578
- var tokenPayload = {
579
- refresh_token : token . refresh_token ,
580
- client_id : this . clientId_ ,
581
- client_secret : this . clientSecret_ ,
582
- grant_type : 'refresh_token'
583
- } ;
584
- if ( this . tokenPayloadHandler_ ) {
585
- tokenPayload = this . tokenPayloadHandler_ ( tokenPayload ) ;
586
- }
587
- // Use the refresh URL if specified, otherwise fallback to the token URL.
588
- var url = this . refreshUrl_ || this . tokenUrl_ ;
589
- var response = UrlFetchApp . fetch ( url , {
590
- method : 'post' ,
591
- headers : headers ,
592
- payload : tokenPayload ,
593
- muteHttpExceptions : true
592
+
593
+ this . lockable_ ( function ( ) {
594
+ var token = this . getToken ( ) ;
595
+ if ( ! token . refresh_token ) {
596
+ throw new Error ( 'Offline access is required.' ) ;
597
+ }
598
+ var headers = {
599
+ Accept : this . tokenFormat_
600
+ } ;
601
+ if ( this . tokenHeaders_ ) {
602
+ headers = extend_ ( headers , this . tokenHeaders_ ) ;
603
+ }
604
+ var tokenPayload = {
605
+ refresh_token : token . refresh_token ,
606
+ client_id : this . clientId_ ,
607
+ client_secret : this . clientSecret_ ,
608
+ grant_type : 'refresh_token'
609
+ } ;
610
+ if ( this . tokenPayloadHandler_ ) {
611
+ tokenPayload = this . tokenPayloadHandler_ ( tokenPayload ) ;
612
+ }
613
+ // Use the refresh URL if specified, otherwise fallback to the token URL.
614
+ var url = this . refreshUrl_ || this . tokenUrl_ ;
615
+ var response = UrlFetchApp . fetch ( url , {
616
+ method : 'post' ,
617
+ headers : headers ,
618
+ payload : tokenPayload ,
619
+ muteHttpExceptions : true
620
+ } ) ;
621
+ var newToken = this . getTokenFromResponse_ ( response ) ;
622
+ if ( ! newToken . refresh_token ) {
623
+ newToken . refresh_token = token . refresh_token ;
624
+ }
625
+ this . saveToken_ ( newToken ) ;
594
626
} ) ;
595
- var newToken = this . getTokenFromResponse_ ( response ) ;
596
- if ( ! newToken . refresh_token ) {
597
- newToken . refresh_token = token . refresh_token ;
598
- }
599
- this . saveToken_ ( newToken ) ;
600
627
} ;
601
628
602
629
/**
@@ -612,7 +639,7 @@ Service_.prototype.getStorage = function() {
612
639
} ) ;
613
640
if ( ! this . storage_ ) {
614
641
var prefix = 'oauth2.' + this . serviceName_ ;
615
- this . storage_ = new Storage ( prefix , this . propertyStore_ , this . cache_ ) ;
642
+ this . storage_ = new Storage_ ( prefix , this . propertyStore_ , this . cache_ ) ;
616
643
}
617
644
return this . storage_ ;
618
645
} ;
@@ -623,17 +650,15 @@ Service_.prototype.getStorage = function() {
623
650
* @private
624
651
*/
625
652
Service_ . prototype . saveToken_ = function ( token ) {
626
- var storage = this . getStorage ( ) ;
627
- storage . setValue ( null , token ) ;
653
+ this . getStorage ( ) . setValue ( null , token ) ;
628
654
} ;
629
655
630
656
/**
631
657
* Gets the token from the service's property store or cache.
632
658
* @return {Object } The token, or null if no token was found.
633
659
*/
634
660
Service_ . prototype . getToken = function ( ) {
635
- var storage = this . getStorage ( ) ;
636
- return storage . getValue ( null ) ;
661
+ return this . getStorage ( ) . getValue ( null ) ;
637
662
} ;
638
663
639
664
/**
@@ -721,6 +746,25 @@ Service_.prototype.createJwt_ = function() {
721
746
return toSign + '.' + signature ;
722
747
} ;
723
748
749
+ /**
750
+ * Locks access to a block of code if a lock has been set on this service.
751
+ * @param {function } func The code to execute.
752
+ * @return {* } The result of the code block.
753
+ * @private
754
+ */
755
+ Service_ . prototype . lockable_ = function ( func ) {
756
+ var releaseLock = false ;
757
+ if ( this . lock_ && ! this . lock_ . hasLock ( ) ) {
758
+ this . lock_ . waitLock ( Service_ . LOCK_EXPIRATION_MILLISECONDS_ ) ;
759
+ releaseLock = true ;
760
+ }
761
+ var result = func . apply ( this ) ;
762
+ if ( this . lock_ && releaseLock ) {
763
+ this . lock_ . releaseLock ( ) ;
764
+ }
765
+ return result ;
766
+ } ;
767
+
724
768
// Copyright 2017 Google Inc. All Rights Reserved.
725
769
//
726
770
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -740,15 +784,16 @@ Service_.prototype.createJwt_ = function() {
740
784
*/
741
785
742
786
/**
743
- * Creates a new storage instance.
787
+ * Creates a new Storage_ instance, which is used to persist OAuth tokens and
788
+ * related information.
744
789
* @param {string } prefix The prefix to use for keys in the properties and
745
790
* cache.
746
791
* @param {PropertiesService.Properties } properties The properties instance to
747
792
* use.
748
- * @param {CacheService.Cache } optCache The optional cache instance to use.
793
+ * @param {CacheService.Cache } [ optCache] The optional cache instance to use.
749
794
* @constructor
750
795
*/
751
- function Storage ( prefix , properties , optCache ) {
796
+ function Storage_ ( prefix , properties , optCache ) {
752
797
this . prefix_ = prefix ;
753
798
this . properties_ = properties ;
754
799
this . cache_ = optCache ;
@@ -760,14 +805,14 @@ function Storage(prefix, properties, optCache) {
760
805
* @type {number }
761
806
* @private
762
807
*/
763
- Storage . CACHE_EXPIRATION_TIME_SECONDS = 21600 ;
808
+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS = 21600 ; // 6 hours.
764
809
765
810
/**
766
811
* Gets a stored value.
767
812
* @param {string } key The key.
768
813
* @return {* } The stored value.
769
814
*/
770
- Storage . prototype . getValue = function ( key ) {
815
+ Storage_ . prototype . getValue = function ( key ) {
771
816
// Check memory.
772
817
if ( this . memory_ [ key ] ) {
773
818
return this . memory_ [ key ] ;
@@ -785,10 +830,10 @@ Storage.prototype.getValue = function(key) {
785
830
}
786
831
787
832
// Check properties.
788
- if ( ( jsonValue = this . properties_ . getProperty ( prefixedKey ) ) ) {
833
+ if ( jsonValue = this . properties_ . getProperty ( prefixedKey ) ) {
789
834
if ( this . cache_ ) {
790
835
this . cache_ . put ( prefixedKey ,
791
- jsonValue , Storage . CACHE_EXPIRATION_TIME_SECONDS ) ;
836
+ jsonValue , Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
792
837
}
793
838
value = JSON . parse ( jsonValue ) ;
794
839
this . memory_ [ key ] = value ;
@@ -804,13 +849,13 @@ Storage.prototype.getValue = function(key) {
804
849
* @param {string } key The key.
805
850
* @param {* } value The value.
806
851
*/
807
- Storage . prototype . setValue = function ( key , value ) {
852
+ Storage_ . prototype . setValue = function ( key , value ) {
808
853
var prefixedKey = this . getPrefixedKey_ ( key ) ;
809
854
var jsonValue = JSON . stringify ( value ) ;
810
855
this . properties_ . setProperty ( prefixedKey , jsonValue ) ;
811
856
if ( this . cache_ ) {
812
857
this . cache_ . put ( prefixedKey , jsonValue ,
813
- Storage . CACHE_EXPIRATION_TIME_SECONDS ) ;
858
+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
814
859
}
815
860
this . memory_ [ key ] = value ;
816
861
} ;
@@ -819,7 +864,7 @@ Storage.prototype.setValue = function(key, value) {
819
864
* Removes a stored value.
820
865
* @param {string } key The key.
821
866
*/
822
- Storage . prototype . removeValue = function ( key ) {
867
+ Storage_ . prototype . removeValue = function ( key ) {
823
868
var prefixedKey = this . getPrefixedKey_ ( key ) ;
824
869
this . properties_ . deleteProperty ( prefixedKey ) ;
825
870
if ( this . cache_ ) {
@@ -834,11 +879,11 @@ Storage.prototype.removeValue = function(key) {
834
879
* @return {string } The key with the prefix applied.
835
880
* @private
836
881
*/
837
- Storage . prototype . getPrefixedKey_ = function ( key ) {
838
- if ( ! key ) {
839
- return this . prefix_ ;
840
- } else {
882
+ Storage_ . prototype . getPrefixedKey_ = function ( key ) {
883
+ if ( key ) {
841
884
return this . prefix_ + '.' + key ;
885
+ } else {
886
+ return this . prefix_ ;
842
887
}
843
888
} ;
844
889
@@ -936,4 +981,4 @@ function copy(src, target, obj) {
936
981
}
937
982
}
938
983
) . call ( null , module . exports , expose , host ) ;
939
- } ) . call ( this , this , "OAuth2" ) ;
984
+ } ) . call ( this , this , "OAuth2" ) ;
0 commit comments