@@ -68,8 +68,8 @@ function createService(serviceName) {
6868 * @return {string } The redirect URI.
6969 */
7070function getRedirectUri ( scriptId ) {
71- return Utilities . formatString (
72- 'https://script.google.com/macros/d/%s/ usercallback', scriptId ) ;
71+ return 'https://script.google.com/macros/d/' + encodeURIComponent ( scriptId ) +
72+ '/ usercallback';
7373}
7474
7575if ( typeof module === 'object' ) {
@@ -459,25 +459,37 @@ Service_.prototype.handleCallback = function(callbackRequest) {
459459 * otherwise.
460460 */
461461Service_ . prototype . hasAccess = function ( ) {
462+ var token = this . getToken ( ) ;
463+ if ( token && ! this . isExpired_ ( token ) ) return true ; // Token still has access.
464+ var canGetToken = ( token && this . canRefresh_ ( token ) ) ||
465+ this . privateKey_ || this . grantType_ ;
466+ if ( ! canGetToken ) return false ;
467+
462468 return this . lockable_ ( function ( ) {
463- var token = this . getToken ( ) ;
464- if ( ! token || this . isExpired_ ( token ) ) {
465- try {
466- if ( token && this . canRefresh_ ( token ) ) {
467- this . refresh ( ) ;
468- } else if ( this . privateKey_ ) {
469- this . exchangeJwt_ ( ) ;
470- } else if ( this . grantType_ ) {
471- this . exchangeGrant_ ( ) ;
472- } else {
473- return false ;
474- }
475- } catch ( e ) {
476- this . lastError_ = e ;
469+ // Get the token again, bypassing the local memory cache.
470+ token = this . getToken ( true ) ;
471+ // Check to see if the token is no longer missing or expired, as another
472+ // execution may have refreshed it while we were waiting for the lock.
473+ if ( token && ! this . isExpired_ ( token ) ) return true ; // Token now has access.
474+ try {
475+ if ( token && this . canRefresh_ ( token ) ) {
476+ this . refresh ( ) ;
477+ return true ;
478+ } else if ( this . privateKey_ ) {
479+ this . exchangeJwt_ ( ) ;
480+ return true ;
481+ } else if ( this . grantType_ ) {
482+ this . exchangeGrant_ ( ) ;
483+ return true ;
484+ } else {
485+ // This should never happen, since canGetToken should have been false
486+ // earlier.
477487 return false ;
478488 }
489+ } catch ( e ) {
490+ this . lastError_ = e ;
491+ return false ;
479492 }
480- return true ;
481493 } ) ;
482494} ;
483495
@@ -662,10 +674,13 @@ Service_.prototype.saveToken_ = function(token) {
662674
663675/**
664676 * Gets the token from the service's property store or cache.
677+ * @param {boolean? } optSkipMemoryCheck If true, bypass the local memory cache
678+ * when fetching the token.
665679 * @return {Object } The token, or null if no token was found.
666680 */
667- Service_ . prototype . getToken = function ( ) {
668- return this . getStorage ( ) . getValue ( null ) ;
681+ Service_ . prototype . getToken = function ( optSkipMemoryCheck ) {
682+ // Gets the stored value under the null key, which is reserved for the token.
683+ return this . getStorage ( ) . getValue ( null , optSkipMemoryCheck ) ;
669684} ;
670685
671686/**
@@ -781,8 +796,8 @@ Service_.prototype.lockable_ = function(func) {
781796
782797/**
783798 * Obtain an access token using the custom grant type specified. Most often
784- * this will be "client_credentials", in which case make sure to also specify an
785- * Authorization header if required by your OAuth provider .
799+ * this will be "client_credentials", and a client ID and secret are set an
800+ * " Authorization: Basic ..." header will be added using those values .
786801 */
787802Service_ . prototype . exchangeGrant_ = function ( ) {
788803 validate_ ( {
@@ -793,6 +808,20 @@ Service_.prototype.exchangeGrant_ = function() {
793808 grant_type : this . grantType_
794809 } ;
795810 payload = extend_ ( payload , this . params_ ) ;
811+
812+ // For the client_credentials grant type, add a basic authorization header:
813+ // - If the client ID and client secret are set.
814+ // - No authorization header has been set yet.
815+ var lowerCaseHeaders = toLowerCaseKeys_ ( this . tokenHeaders_ ) ;
816+ if ( this . grantType_ === 'client_credentials' &&
817+ this . clientId_ &&
818+ this . clientSecret_ &&
819+ ( ! lowerCaseHeaders || ! lowerCaseHeaders . authorization ) ) {
820+ this . tokenHeaders_ = this . tokenHeaders_ || { } ;
821+ this . tokenHeaders_ . authorization = 'Basic ' +
822+ Utilities . base64Encode ( this . clientId_ + ':' + this . clientSecret_ ) ;
823+ }
824+
796825 var token = this . fetchToken_ ( payload ) ;
797826 this . saveToken_ ( token ) ;
798827} ;
@@ -839,25 +868,42 @@ function Storage_(prefix, properties, optCache) {
839868 */
840869Storage_ . CACHE_EXPIRATION_TIME_SECONDS = 21600 ; // 6 hours.
841870
871+ /**
872+ * The special value to use in the cache to indicate that there is no value.
873+ * @type {string }
874+ * @private
875+ */
876+ Storage_ . CACHE_NULL_VALUE = '__NULL__' ;
877+
842878/**
843879 * Gets a stored value.
844880 * @param {string } key The key.
881+ * @param {boolean? } optSkipMemoryCheck Whether to bypass the local memory cache
882+ * when fetching the value (the default is false).
845883 * @return {* } The stored value.
846884 */
847- Storage_ . prototype . getValue = function ( key ) {
848- // Check memory.
849- if ( this . memory_ [ key ] ) {
850- return this . memory_ [ key ] ;
851- }
852-
885+ Storage_ . prototype . getValue = function ( key , optSkipMemoryCheck ) {
853886 var prefixedKey = this . getPrefixedKey_ ( key ) ;
854887 var jsonValue ;
855888 var value ;
856889
890+ if ( ! optSkipMemoryCheck ) {
891+ // Check in-memory cache.
892+ if ( value = this . memory_ [ key ] ) {
893+ if ( value === Storage_ . CACHE_NULL_VALUE ) {
894+ return null ;
895+ }
896+ return value ;
897+ }
898+ }
899+
857900 // Check cache.
858901 if ( this . cache_ && ( jsonValue = this . cache_ . get ( prefixedKey ) ) ) {
859902 value = JSON . parse ( jsonValue ) ;
860903 this . memory_ [ key ] = value ;
904+ if ( value === Storage_ . CACHE_NULL_VALUE ) {
905+ return null ;
906+ }
861907 return value ;
862908 }
863909
@@ -872,7 +918,13 @@ Storage_.prototype.getValue = function(key) {
872918 return value ;
873919 }
874920
875- // Not found.
921+ // Not found. Store a special null value in the memory and cache to reduce
922+ // hits on the PropertiesService.
923+ this . memory_ [ key ] = Storage_ . CACHE_NULL_VALUE ;
924+ if ( this . cache_ ) {
925+ this . cache_ . put ( prefixedKey , JSON . stringify ( Storage_ . CACHE_NULL_VALUE ) ,
926+ Storage_ . CACHE_EXPIRATION_TIME_SECONDS ) ;
927+ }
876928 return null ;
877929} ;
878930
@@ -998,6 +1050,25 @@ function extend_(destination, source) {
9981050 return destination ;
9991051}
10001052
1053+ /* exported toLowerCaseKeys_ */
1054+ /**
1055+ * Gets a copy of an object with all the keys converted to lower-case strings.
1056+ *
1057+ * @param {Object } obj The object to copy.
1058+ * @return {Object } a shallow copy of the object with all lower-case keys.
1059+ */
1060+ function toLowerCaseKeys_ ( obj ) {
1061+ if ( obj === null || typeof obj !== 'object' ) {
1062+ return obj ;
1063+ }
1064+ // For each key in the source object, add a lower-case version to a new
1065+ // object, and return it.
1066+ return Object . keys ( obj ) . reduce ( function ( result , k ) {
1067+ result [ k . toLowerCase ( ) ] = obj [ k ] ;
1068+ return result ;
1069+ } , { } ) ;
1070+ }
1071+
10011072 /****** code end *********/
10021073 ; (
10031074function copy ( src , target , obj ) {
0 commit comments