Skip to content

Commit c6ce8b3

Browse files
committed
Adding support for setTokenPayloadHandler to support Smartsheet API.
1 parent 9c36df6 commit c6ce8b3

File tree

3 files changed

+110
-33
lines changed

3 files changed

+110
-33
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,39 @@ in these requests.
188188

189189
See the [FitBit sample](samples/FitBit.gs) for the compelte code.
190190

191+
#### Modifying the access token payload
192+
Similar to Setting additional token headers, some services, such as the Smartsheet API, require you to [add a hash to the access token request payloads](http://smartsheet-platform.github.io/api-docs/?javascript#oauth-flow). The `setTokenPayloadHandler` method allows you to pass in a function to modify the payload of an access token request before the request is sent to the token endpoint:
193+
194+
195+
// Set the handler for adding Smartsheet's required SHA hash parameter to the payload:
196+
.setTokenPayloadHandler(smartsheetTokenHandler)
197+
...
198+
function smartsheetTokenHandler(payload) {
199+
var codeOrRefreshToken = payload.code ? payload.code : payload.refresh_token;
200+
var input = SMARTSHEET_CLIENT_SECRET + "|" + codeOrRefreshToken;
201+
var hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
202+
input,
203+
Utilities.Charset.UTF_8);
204+
hash = hash.map(function(val) {
205+
// Google appears to treat these as signed bytes, but we need them unsigned...
206+
if (val < 0)
207+
val += 256;
208+
var str = val.toString(16);
209+
// pad to two hex digits:
210+
if (str.length == 1)
211+
str = '0' + str;
212+
return str;
213+
});
214+
payload.hash = hash.join("");
215+
// Smartsheet doesn't need the client secret sent (secret is verified by the hash)
216+
if (payload.client_secret) {
217+
delete payload.client_secret;
218+
}
219+
return payload;
220+
}
221+
222+
223+
191224
#### Service Accounts
192225

193226
This library supports the service account authorization flow, also known as the

dist/OAuth2.gs

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ if (module) {
6060
module.exports = {
6161
createService: createService,
6262
getRedirectUri: getRedirectUri
63-
}
63+
};
6464
}
6565

6666
// Copyright 2014 Google Inc. All Rights Reserved.
@@ -81,6 +81,10 @@ if (module) {
8181
* @fileoverview Contains the Service_ class.
8282
*/
8383

84+
// Disable JSHint warnings for the use of eval(), since it's required to prevent
85+
// scope issues in Apps Script.
86+
// jshint evil:true
87+
8488
/**
8589
* Creates a new OAuth2 service.
8690
* @param {string} serviceName The name of the service.
@@ -148,6 +152,16 @@ Service_.prototype.setTokenHeaders = function(tokenHeaders) {
148152
return this;
149153
};
150154

155+
/**
156+
* Sets an additional function to invoke on the payload of the access token request.
157+
* @param Object tokenHandler A function to invoke on the payload of the request for an access token.
158+
* @return {Service_} This service, for chaining.
159+
*/
160+
Service_.prototype.setTokenPayloadHandler = function(tokenHandler) {
161+
this.tokenPayloadHandler_ = tokenHandler;
162+
return this;
163+
};
164+
151165
/**
152166
* Sets the project key of the script that contains the authorization callback function (required).
153167
* The project key can be found in the Script Editor UI under "File > Project properties".
@@ -235,7 +249,7 @@ Service_.prototype.setCache = function(cache) {
235249
*/
236250
Service_.prototype.setScope = function(scope, opt_separator) {
237251
var separator = opt_separator || ' ';
238-
this.params_['scope'] = _.isArray(scope) ? scope.join(separator) : scope;
252+
this.params_.scope = _.isArray(scope) ? scope.join(separator) : scope;
239253
return this;
240254
};
241255

@@ -352,16 +366,21 @@ Service_.prototype.handleCallback = function(callbackRequest) {
352366
if (this.tokenHeaders_) {
353367
headers = _.extend(headers, this.tokenHeaders_);
354368
}
369+
var tokenPayload = {
370+
code: code,
371+
client_id: this.clientId_,
372+
client_secret: this.clientSecret_,
373+
redirect_uri: redirectUri,
374+
grant_type: 'authorization_code'
375+
};
376+
if (this.tokenPayloadHandler_) {
377+
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
378+
Logger.log('Token payload from tokenPayloadHandler: %s', JSON.stringify(tokenPayload));
379+
}
355380
var response = UrlFetchApp.fetch(this.tokenUrl_, {
356381
method: 'post',
357382
headers: headers,
358-
payload: {
359-
code: code,
360-
client_id: this.clientId_,
361-
client_secret: this.clientSecret_,
362-
redirect_uri: redirectUri,
363-
grant_type: 'authorization_code'
364-
},
383+
payload: tokenPayload,
365384
muteHttpExceptions: true
366385
});
367386
var token = this.getTokenFromResponse_(response);
@@ -441,7 +460,7 @@ Service_.prototype.getLastError = function() {
441460
Service_.prototype.getTokenFromResponse_ = function(response) {
442461
var token = this.parseToken_(response.getContentText());
443462
if (response.getResponseCode() != 200 || token.error) {
444-
var reason = [token.error, token.error_description, token.error_uri].filter(Boolean).join(', ');
463+
var reason = [token.error, token.message, token.error_description, token.error_uri].filter(Boolean).join(', ');
445464
if (!reason) {
446465
reason = response.getResponseCode() + ': ' + JSON.stringify(token);
447466
}
@@ -464,7 +483,7 @@ Service_.prototype.parseToken_ = function(content) {
464483
} catch (e) {
465484
throw 'Token response not valid JSON: ' + e;
466485
}
467-
} else if (this.tokenFormat_ = TOKEN_FORMAT.FORM_URL_ENCODED) {
486+
} else if (this.tokenFormat_ == TOKEN_FORMAT.FORM_URL_ENCODED) {
468487
token = content.split('&').reduce(function(result, pair) {
469488
var parts = pair.split('=');
470489
result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
@@ -497,15 +516,20 @@ Service_.prototype.refresh = function() {
497516
if (this.tokenHeaders_) {
498517
headers = _.extend(headers, this.tokenHeaders_);
499518
}
500-
var response = UrlFetchApp.fetch(this.tokenUrl_, {
501-
method: 'post',
502-
headers: headers,
503-
payload: {
519+
var tokenPayload = {
504520
refresh_token: token.refresh_token,
505521
client_id: this.clientId_,
506522
client_secret: this.clientSecret_,
507523
grant_type: 'refresh_token'
508-
},
524+
};
525+
if (this.tokenPayloadHandler_) {
526+
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
527+
Logger.log('Token payload from tokenPayloadHandler (refresh): %s', JSON.stringify(tokenPayload));
528+
}
529+
var response = UrlFetchApp.fetch(this.tokenUrl_, {
530+
method: 'post',
531+
headers: headers,
532+
payload: tokenPayload,
509533
muteHttpExceptions: true
510534
});
511535
var newToken = this.getTokenFromResponse_(response);
@@ -639,10 +663,10 @@ Service_.prototype.createJwt_ = function() {
639663
iat: Math.round(now.getTime() / 1000)
640664
};
641665
if (this.subject_) {
642-
claimSet['sub'] = this.subject_;
666+
claimSet.sub = this.subject_;
643667
}
644-
if (this.params_['scope']) {
645-
claimSet['scope'] = this.params_['scope'];
668+
if (this.params_.scope) {
669+
claimSet.scope = this.params_.scope;
646670
}
647671
var toSign = Utilities.base64EncodeWebSafe(JSON.stringify(header)) + '.' + Utilities.base64EncodeWebSafe(JSON.stringify(claimSet));
648672
var signatureBytes = Utilities.computeRsaSha256Signature(toSign, this.privateKey_);
@@ -705,7 +729,7 @@ function validate_(params) {
705729
* @private
706730
*/
707731
function isEmpty_(value) {
708-
return value == null || value == undefined ||
732+
return value === null || value === undefined ||
709733
((_.isObject(value) || _.isString(value)) && _.isEmpty(value));
710734
}
711735

src/Service.gs

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ Service_.prototype.setTokenHeaders = function(tokenHeaders) {
8787
return this;
8888
};
8989

90+
/**
91+
* Sets an additional function to invoke on the payload of the access token request.
92+
* @param Object tokenHandler A function to invoke on the payload of the request for an access token.
93+
* @return {Service_} This service, for chaining.
94+
*/
95+
Service_.prototype.setTokenPayloadHandler = function(tokenHandler) {
96+
this.tokenPayloadHandler_ = tokenHandler;
97+
return this;
98+
};
99+
90100
/**
91101
* Sets the project key of the script that contains the authorization callback function (required).
92102
* The project key can be found in the Script Editor UI under "File > Project properties".
@@ -291,16 +301,21 @@ Service_.prototype.handleCallback = function(callbackRequest) {
291301
if (this.tokenHeaders_) {
292302
headers = _.extend(headers, this.tokenHeaders_);
293303
}
304+
var tokenPayload = {
305+
code: code,
306+
client_id: this.clientId_,
307+
client_secret: this.clientSecret_,
308+
redirect_uri: redirectUri,
309+
grant_type: 'authorization_code'
310+
};
311+
if (this.tokenPayloadHandler_) {
312+
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
313+
Logger.log('Token payload from tokenPayloadHandler: %s', JSON.stringify(tokenPayload));
314+
}
294315
var response = UrlFetchApp.fetch(this.tokenUrl_, {
295316
method: 'post',
296317
headers: headers,
297-
payload: {
298-
code: code,
299-
client_id: this.clientId_,
300-
client_secret: this.clientSecret_,
301-
redirect_uri: redirectUri,
302-
grant_type: 'authorization_code'
303-
},
318+
payload: tokenPayload,
304319
muteHttpExceptions: true
305320
});
306321
var token = this.getTokenFromResponse_(response);
@@ -380,7 +395,7 @@ Service_.prototype.getLastError = function() {
380395
Service_.prototype.getTokenFromResponse_ = function(response) {
381396
var token = this.parseToken_(response.getContentText());
382397
if (response.getResponseCode() != 200 || token.error) {
383-
var reason = [token.error, token.error_description, token.error_uri].filter(Boolean).join(', ');
398+
var reason = [token.error, token.message, token.error_description, token.error_uri].filter(Boolean).join(', ');
384399
if (!reason) {
385400
reason = response.getResponseCode() + ': ' + JSON.stringify(token);
386401
}
@@ -436,15 +451,20 @@ Service_.prototype.refresh = function() {
436451
if (this.tokenHeaders_) {
437452
headers = _.extend(headers, this.tokenHeaders_);
438453
}
439-
var response = UrlFetchApp.fetch(this.tokenUrl_, {
440-
method: 'post',
441-
headers: headers,
442-
payload: {
454+
var tokenPayload = {
443455
refresh_token: token.refresh_token,
444456
client_id: this.clientId_,
445457
client_secret: this.clientSecret_,
446458
grant_type: 'refresh_token'
447-
},
459+
};
460+
if (this.tokenPayloadHandler_) {
461+
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
462+
Logger.log('Token payload from tokenPayloadHandler (refresh): %s', JSON.stringify(tokenPayload));
463+
}
464+
var response = UrlFetchApp.fetch(this.tokenUrl_, {
465+
method: 'post',
466+
headers: headers,
467+
payload: tokenPayload,
448468
muteHttpExceptions: true
449469
});
450470
var newToken = this.getTokenFromResponse_(response);

0 commit comments

Comments
 (0)