@@ -128,6 +128,13 @@ var Service_ = function(serviceName) {
128128 */
129129Service_ . EXPIRATION_BUFFER_SECONDS_ = 60 ;
130130
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+
131138/**
132139 * Sets the service's authorization base URL (required). For Google services
133140 * this URL should be
@@ -257,6 +264,7 @@ Service_.prototype.setClientSecret = function(clientSecret) {
257264 * @param {PropertiesService.Properties } propertyStore The property store to use
258265 * when persisting credentials.
259266 * @return {Service_ } This service, for chaining.
267+ * @see https://developers.google.com/apps-script/reference/properties/
260268 */
261269Service_ . prototype . setPropertyStore = function ( propertyStore ) {
262270 this . propertyStore_ = propertyStore ;
@@ -271,12 +279,27 @@ Service_.prototype.setPropertyStore = function(propertyStore) {
271279 * @param {CacheService.Cache } cache The cache to use when persisting
272280 * credentials.
273281 * @return {Service_ } This service, for chaining.
282+ * @see https://developers.google.com/apps-script/reference/cache/
274283 */
275284Service_ . prototype . setCache = function ( cache ) {
276285 this . cache_ = cache ;
277286 return this ;
278287} ;
279288
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+
280303/**
281304 * Sets the scope or scopes to request during the authorization flow (optional).
282305 * If the scope value is an array it will be joined using the separator before
@@ -437,27 +460,29 @@ Service_.prototype.handleCallback = function(callbackRequest) {
437460 * otherwise.
438461 */
439462Service_ . 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 {
454481 return false ;
455482 }
456- } else {
457- return false ;
458483 }
459- }
460- return true ;
484+ return true ;
485+ } ) ;
461486} ;
462487
463488/**
@@ -479,8 +504,7 @@ Service_.prototype.getAccessToken = function() {
479504 * re-authorized.
480505 */
481506Service_ . prototype . reset = function ( ) {
482- var storage = this . getStorage ( ) ;
483- storage . removeValue ( null ) ;
507+ this . getStorage ( ) . removeValue ( null ) ;
484508} ;
485509
486510/**
@@ -565,38 +589,41 @@ Service_.prototype.refresh = function() {
565589 'Client Secret' : this . clientSecret_ ,
566590 'Token URL' : this . tokenUrl_
567591 } ) ;
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 ) ;
594626 } ) ;
595- var newToken = this . getTokenFromResponse_ ( response ) ;
596- if ( ! newToken . refresh_token ) {
597- newToken . refresh_token = token . refresh_token ;
598- }
599- this . saveToken_ ( newToken ) ;
600627} ;
601628
602629/**
@@ -612,7 +639,7 @@ Service_.prototype.getStorage = function() {
612639 } ) ;
613640 if ( ! this . storage_ ) {
614641 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_ ) ;
616643 }
617644 return this . storage_ ;
618645} ;
@@ -623,17 +650,15 @@ Service_.prototype.getStorage = function() {
623650 * @private
624651 */
625652Service_ . prototype . saveToken_ = function ( token ) {
626- var storage = this . getStorage ( ) ;
627- storage . setValue ( null , token ) ;
653+ this . getStorage ( ) . setValue ( null , token ) ;
628654} ;
629655
630656/**
631657 * Gets the token from the service's property store or cache.
632658 * @return {Object } The token, or null if no token was found.
633659 */
634660Service_ . prototype . getToken = function ( ) {
635- var storage = this . getStorage ( ) ;
636- return storage . getValue ( null ) ;
661+ return this . getStorage ( ) . getValue ( null ) ;
637662} ;
638663
639664/**
@@ -721,6 +746,25 @@ Service_.prototype.createJwt_ = function() {
721746 return toSign + '.' + signature ;
722747} ;
723748
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+
724768// Copyright 2017 Google Inc. All Rights Reserved.
725769//
726770// Licensed under the Apache License, Version 2.0 (the "License");
@@ -740,15 +784,16 @@ Service_.prototype.createJwt_ = function() {
740784 */
741785
742786/**
743- * Creates a new storage instance.
787+ * Creates a new Storage_ instance, which is used to persist OAuth tokens and
788+ * related information.
744789 * @param {string } prefix The prefix to use for keys in the properties and
745790 * cache.
746791 * @param {PropertiesService.Properties } properties The properties instance to
747792 * use.
748- * @param {CacheService.Cache } optCache The optional cache instance to use.
793+ * @param {CacheService.Cache } [ optCache] The optional cache instance to use.
749794 * @constructor
750795 */
751- function Storage ( prefix , properties , optCache ) {
796+ function Storage_ ( prefix , properties , optCache ) {
752797 this . prefix_ = prefix ;
753798 this . properties_ = properties ;
754799 this . cache_ = optCache ;
@@ -760,14 +805,14 @@ function Storage(prefix, properties, optCache) {
760805 * @type {number }
761806 * @private
762807 */
763- Storage . CACHE_EXPIRATION_TIME_SECONDS = 21600 ;
808+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS = 21600 ; // 6 hours.
764809
765810/**
766811 * Gets a stored value.
767812 * @param {string } key The key.
768813 * @return {* } The stored value.
769814 */
770- Storage . prototype . getValue = function ( key ) {
815+ Storage_ . prototype . getValue = function ( key ) {
771816 // Check memory.
772817 if ( this . memory_ [ key ] ) {
773818 return this . memory_ [ key ] ;
@@ -785,10 +830,10 @@ Storage.prototype.getValue = function(key) {
785830 }
786831
787832 // Check properties.
788- if ( ( jsonValue = this . properties_ . getProperty ( prefixedKey ) ) ) {
833+ if ( jsonValue = this . properties_ . getProperty ( prefixedKey ) ) {
789834 if ( this . cache_ ) {
790835 this . cache_ . put ( prefixedKey ,
791- jsonValue , Storage . CACHE_EXPIRATION_TIME_SECONDS ) ;
836+ jsonValue , Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
792837 }
793838 value = JSON . parse ( jsonValue ) ;
794839 this . memory_ [ key ] = value ;
@@ -804,13 +849,13 @@ Storage.prototype.getValue = function(key) {
804849 * @param {string } key The key.
805850 * @param {* } value The value.
806851 */
807- Storage . prototype . setValue = function ( key , value ) {
852+ Storage_ . prototype . setValue = function ( key , value ) {
808853 var prefixedKey = this . getPrefixedKey_ ( key ) ;
809854 var jsonValue = JSON . stringify ( value ) ;
810855 this . properties_ . setProperty ( prefixedKey , jsonValue ) ;
811856 if ( this . cache_ ) {
812857 this . cache_ . put ( prefixedKey , jsonValue ,
813- Storage . CACHE_EXPIRATION_TIME_SECONDS ) ;
858+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
814859 }
815860 this . memory_ [ key ] = value ;
816861} ;
@@ -819,7 +864,7 @@ Storage.prototype.setValue = function(key, value) {
819864 * Removes a stored value.
820865 * @param {string } key The key.
821866 */
822- Storage . prototype . removeValue = function ( key ) {
867+ Storage_ . prototype . removeValue = function ( key ) {
823868 var prefixedKey = this . getPrefixedKey_ ( key ) ;
824869 this . properties_ . deleteProperty ( prefixedKey ) ;
825870 if ( this . cache_ ) {
@@ -834,11 +879,11 @@ Storage.prototype.removeValue = function(key) {
834879 * @return {string } The key with the prefix applied.
835880 * @private
836881 */
837- Storage . prototype . getPrefixedKey_ = function ( key ) {
838- if ( ! key ) {
839- return this . prefix_ ;
840- } else {
882+ Storage_ . prototype . getPrefixedKey_ = function ( key ) {
883+ if ( key ) {
841884 return this . prefix_ + '.' + key ;
885+ } else {
886+ return this . prefix_ ;
842887 }
843888} ;
844889
@@ -936,4 +981,4 @@ function copy(src, target, obj) {
936981 }
937982}
938983 ) . call ( null , module . exports , expose , host ) ;
939- } ) . call ( this , this , "OAuth2" ) ;
984+ } ) . call ( this , this , "OAuth2" ) ;
0 commit comments