Skip to content

Commit aa7f519

Browse files
author
Eric Koleda
committed
Merge pull request #38 from activescott/add-setTokenPayloadHandler
Adding support for setTokenPayloadHandler to support Smartsheet API.
2 parents 9c36df6 + 98fabae commit aa7f519

File tree

4 files changed

+185
-34
lines changed

4 files changed

+185
-34
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,16 @@ in these requests.
186186
'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET)
187187
});
188188

189-
See the [FitBit sample](samples/FitBit.gs) for the compelte code.
189+
See the [FitBit sample](samples/FitBit.gs) for the complete code.
190+
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 modifying the access token request payload:
196+
.setTokenPayloadHandler(myTokenHandler)
197+
198+
See the [Smartsheet sample](samples/Smartsheet.gs) for the complete code.
190199

191200
#### Service Accounts
192201

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

samples/Smartsheet.gs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
var CLIENT_ID = '...';
2+
var CLIENT_SECRET = '...';
3+
4+
/**
5+
* Authorizes and makes a request to the GitHub API.
6+
*/
7+
function run() {
8+
var service = getService();
9+
if (service.hasAccess()) {
10+
var url = 'https://api.smartsheet.com/2.0/users/me';
11+
var response = UrlFetchApp.fetch(url, {
12+
headers: {
13+
Authorization: 'Bearer ' + service.getAccessToken()
14+
}
15+
});
16+
var result = JSON.parse(response.getContentText());
17+
Logger.log(JSON.stringify(result, null, 2));
18+
} else {
19+
var authorizationUrl = service.getAuthorizationUrl();
20+
Logger.log('Open the following URL and re-run the script: %s',
21+
authorizationUrl);
22+
}
23+
}
24+
25+
/**
26+
* Reset the authorization state, so that it can be re-tested.
27+
*/
28+
function reset() {
29+
var service = getService();
30+
service.reset();
31+
}
32+
33+
/**
34+
* Configures the service.
35+
*/
36+
function getService() {
37+
return OAuth2.createService('Smartsheet')
38+
// Set the endpoint URLs.
39+
.setAuthorizationBaseUrl('https://app.smartsheet.com/b/authorize')
40+
.setTokenUrl('https://api.smartsheet.com/2.0/token')
41+
42+
// Set the client ID and secret.
43+
.setClientId(CLIENT_ID)
44+
.setClientSecret(CLIENT_SECRET)
45+
46+
// Set the name of the callback function that should be invoked to complete
47+
// the OAuth flow.
48+
.setCallbackFunction('authCallback')
49+
50+
// Set the property store where authorized tokens should be persisted.
51+
.setPropertyStore(PropertiesService.getUserProperties())
52+
53+
// Scopes to request
54+
.setScope('READ_SHEETS')
55+
56+
// Set the handler for adding Smartsheet's required SHA hash parameter to the payload:
57+
.setTokenPayloadHandler(smartsheetTokenHandler)
58+
}
59+
60+
/**
61+
* Handles the OAuth callback.
62+
*/
63+
function authCallback(request) {
64+
var service = getService();
65+
var authorized = service.handleCallback(request);
66+
if (authorized) {
67+
return HtmlService.createHtmlOutput('Success!');
68+
} else {
69+
return HtmlService.createHtmlOutput('Denied');
70+
}
71+
}
72+
73+
/**
74+
* Adds the Smartsheet API's required SHA256 hash parameter to the access token request payload.
75+
*/
76+
function smartsheetTokenHandler(payload) {
77+
var codeOrRefreshToken = payload.code ? payload.code : payload.refresh_token;
78+
var input = CLIENT_SECRET + "|" + codeOrRefreshToken;
79+
var hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
80+
input,
81+
Utilities.Charset.UTF_8);
82+
hash = hash.map(function(val) {
83+
// Google appears to treat these as signed bytes, but we need them unsigned...
84+
if (val < 0)
85+
val += 256;
86+
var str = val.toString(16);
87+
// pad to two hex digits:
88+
if (str.length == 1)
89+
str = '0' + str;
90+
return str;
91+
});
92+
payload.hash = hash.join("");
93+
// The Smartsheet API doesn't need the client secret sent (secret is verified by the hash)
94+
if (payload.client_secret) {
95+
delete payload.client_secret;
96+
}
97+
return payload;
98+
}

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)