@@ -30,6 +30,7 @@ var Service_ = function(serviceName) {
3030 this . tokenFormat_ = TOKEN_FORMAT . JSON ;
3131 this . tokenHeaders_ = null ;
3232 this . projectKey_ = eval ( 'Script' + 'App' ) . getProjectKey ( ) ;
33+ this . expirationMinutes_ = 60 ;
3334} ;
3435
3536/**
@@ -72,7 +73,7 @@ Service_.prototype.setTokenFormat = function(tokenFormat) {
7273} ;
7374
7475/**
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
7677 * refreshing the access token.
7778 * @param Object.<string,string> tokenHeaders A map of header names to values.
7879 * @return {Service_ } This service, for chaining.
@@ -185,6 +186,48 @@ Service_.prototype.setParam = function(name, value) {
185186 return this ;
186187} ;
187188
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+
188231/**
189232 * Gets the authorization URL. The first step in getting an OAuth2 token is to
190233 * have the user visit this URL and approve the authorization request. The
@@ -273,23 +316,23 @@ Service_.prototype.handleCallback = function(callbackRequest) {
273316 */
274317Service_ . prototype . hasAccess = function ( ) {
275318 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 ;
291325 return false ;
292326 }
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 ;
293336 }
294337 }
295338 return true ;
@@ -316,7 +359,16 @@ Service_.prototype.reset = function() {
316359 validate_ ( {
317360 'Property store' : this . propertyStore_
318361 } ) ;
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_ ;
320372} ;
321373
322374/**
@@ -397,7 +449,7 @@ Service_.prototype.saveToken_ = function(token) {
397449 validate_ ( {
398450 'Property store' : this . propertyStore_
399451 } ) ;
400- var key = this . getPropertyKey ( this . serviceName_ ) ;
452+ var key = this . getPropertyKey_ ( this . serviceName_ ) ;
401453 var value = JSON . stringify ( token ) ;
402454 this . propertyStore_ . setProperty ( key , value ) ;
403455 if ( this . cache_ ) {
@@ -414,7 +466,7 @@ Service_.prototype.getToken_ = function() {
414466 validate_ ( {
415467 'Property store' : this . propertyStore_
416468 } ) ;
417- var key = this . getPropertyKey ( this . serviceName_ ) ;
469+ var key = this . getPropertyKey_ ( this . serviceName_ ) ;
418470 var token ;
419471 if ( this . cache_ ) {
420472 token = this . cache_ . get ( key ) ;
@@ -438,6 +490,91 @@ Service_.prototype.getToken_ = function() {
438490 * @return {string } The property key.
439491 * @private
440492 */
441- Service_ . prototype . getPropertyKey = function ( serviceName ) {
493+ Service_ . prototype . getPropertyKey_ = function ( serviceName ) {
442494 return 'oauth2.' + serviceName ;
443495} ;
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