Skip to content

Commit 5673122

Browse files
author
Eric Koleda
committed
Release version 25.
1 parent d608daa commit 5673122

File tree

3 files changed

+118
-73
lines changed

3 files changed

+118
-73
lines changed

dist/OAuth2.gs

+116-71
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ var Service_ = function(serviceName) {
128128
*/
129129
Service_.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
*/
261269
Service_.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
*/
275284
Service_.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
*/
439462
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 {
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
*/
481506
Service_.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
*/
625652
Service_.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
*/
634660
Service_.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");

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apps-script-oauth2",
3-
"version": "1.24.0",
3+
"version": "1.25.0",
44
"description": "OAuth2 for Apps Script is a library for Google Apps Script that provides the ability to create and authorize OAuth2 tokens as well as refresh them when they expire.",
55
"repository": {
66
"type": "git",

0 commit comments

Comments
 (0)