Skip to content

Commit c4f13e6

Browse files
author
Eric Koleda
authored
Merge pull request #155 from gsuitedevs/params
Add support for custom callback parameters.
2 parents bcad494 + 90de84c commit c4f13e6

File tree

5 files changed

+107
-11
lines changed

5 files changed

+107
-11
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,39 @@ if (service.hasAccess()) {
325325
Note that calling `Service.reset()` will remove all custom values from storage,
326326
in addition to the token.
327327
328+
#### Passing additional parameters to the callback function
329+
330+
There are occasionally cases where you need to preserve some data through the
331+
OAuth flow, so that it is available in your callback function. Although you
332+
could use the token storage mechanism discussed above for that purpose, writing
333+
to the PropertiesService is expensive and not neccessary in the case where the
334+
user doesn't start or fails to complete the OAuth flow.
335+
336+
As an alternative you can store small amounts of data in the OAuth2 `state`
337+
token, which is a standard mechanism for this purpose. To do so, pass an
338+
optional hash of parameter names and values to the `getAuthorizationUrl()`
339+
method:
340+
341+
```js
342+
var authorizationUrl = getService().getAuthorizationUrl({
343+
// Pass the additional parameter "lang" with the value "fr".
344+
lang: 'fr'
345+
});
346+
```
347+
348+
These values will be stored along-side Apps Script's internal information in the
349+
encypted `state` token, which is passed in the authorization URL and passed back
350+
to the redirect URI. The `state` token is automatically decrypted in the
351+
callback function and you can access your parameters using the same
352+
`request.parameter` field used in web apps:
353+
354+
```js
355+
function authCallback(request) {
356+
var lang = request.parameter.lang;
357+
// ...
358+
}
359+
```
360+
328361
#### Using service accounts
329362
330363
This library supports the service account authorization flow, also known as the

src/Service.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,11 @@ Service_.prototype.setGrantType = function(grantType) {
306306
* have the user visit this URL and approve the authorization request. The
307307
* user will then be redirected back to your application using callback function
308308
* name specified, so that the flow may continue.
309+
* @param {Object} optAdditionalParameters Additional parameters that should be
310+
* stored in the state token and made available in the callback function.
309311
* @return {string} The authorization URL.
310312
*/
311-
Service_.prototype.getAuthorizationUrl = function() {
313+
Service_.prototype.getAuthorizationUrl = function(optAdditionalParameters) {
312314
validate_({
313315
'Client ID': this.clientId_,
314316
'Script ID': this.scriptId_,
@@ -317,16 +319,20 @@ Service_.prototype.getAuthorizationUrl = function() {
317319
});
318320

319321
var redirectUri = getRedirectUri(this.scriptId_);
320-
var state = eval('Script' + 'App').newStateToken()
322+
var stateTokenBuilder = eval('Script' + 'App').newStateToken()
321323
.withMethod(this.callbackFunctionName_)
322324
.withArgument('serviceName', this.serviceName_)
323-
.withTimeout(3600)
324-
.createToken();
325+
.withTimeout(3600);
326+
if (optAdditionalParameters) {
327+
Object.keys(optAdditionalParameters).forEach(function(key) {
328+
stateTokenBuilder.withArgument(key, optAdditionalParameters[key]);
329+
});
330+
}
325331
var params = {
326332
client_id: this.clientId_,
327333
response_type: 'code',
328334
redirect_uri: redirectUri,
329-
state: state
335+
state: stateTokenBuilder.createToken()
330336
};
331337
params = extend_(params, this.params_);
332338
return buildUrl_(this.authorizationBaseUrl_, params);

src/Utilities.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function validate_(params) {
4242
Object.keys(params).forEach(function(name) {
4343
var value = params[name];
4444
if (!value) {
45-
throw Utilities.formatString('%s is required.', name);
45+
throw new Error(name + ' is required.');
4646
}
4747
});
4848
}

test/mocks/script.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
var MockScriptApp = function() {};
2+
3+
MockScriptApp.prototype.getScriptId = function() {
4+
return Math.random().toString(36).substring(2);
5+
};
6+
7+
MockScriptApp.prototype.newStateToken = function() {
8+
return new MockStateTokenBuilder();
9+
};
10+
11+
var MockStateTokenBuilder = function() {
12+
this.arguments = {};
13+
};
14+
15+
MockStateTokenBuilder.prototype.withMethod = function(method) {
16+
this.method = method;
17+
return this;
18+
};
19+
20+
MockStateTokenBuilder.prototype.withArgument = function(key, value) {
21+
this.arguments[key] = value;
22+
return this;
23+
};
24+
25+
MockStateTokenBuilder.prototype.withTimeout = function(timeout) {
26+
this.timeout = timeout;
27+
return this;
28+
};
29+
30+
MockStateTokenBuilder.prototype.createToken = function() {
31+
return JSON.stringify(this);
32+
};
33+
34+
module.exports = MockScriptApp;

test/test.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,11 @@ var MockUrlFetchApp = require('./mocks/urlfetchapp');
44
var MockProperties = require('./mocks/properties');
55
var MockCache = require('./mocks/cache');
66
var MockLock = require('./mocks/lock');
7+
var MockScriptApp = require('./mocks/script');
78
var Future = require('fibers/future');
89

910
var mocks = {
10-
ScriptApp: {
11-
getScriptId: function() {
12-
return '12345';
13-
}
14-
},
11+
ScriptApp: new MockScriptApp(),
1512
UrlFetchApp: new MockUrlFetchApp(),
1613
Utilities: {
1714
base64Encode: function(data) {
@@ -405,6 +402,32 @@ describe('Service', function() {
405402
service.exchangeGrant_();
406403
});
407404
});
405+
406+
describe('#getAuthorizationUrl()', function() {
407+
it('should add additional parameters to the state token', function() {
408+
var service = OAuth2.createService('test')
409+
.setAuthorizationBaseUrl('http://www.example.com')
410+
.setClientId('abc')
411+
.setClientSecret('def')
412+
.setCallbackFunction('authCallback');
413+
var authorizationUrl = service.getAuthorizationUrl({
414+
foo: 'bar'
415+
});
416+
417+
// Extract the state token from the URL and parse it. For example, the
418+
// URL http://www.example.com?state=%7B%22a%22%3A1%7D would produce
419+
// {a: 1}.
420+
var querystring = authorizationUrl.split('?')[1];
421+
var params = querystring.split('&').reduce(function(result, pair) {
422+
var parts = pair.split('=').map(decodeURIComponent);
423+
result[parts[0]] = parts[1];
424+
return result;
425+
}, {});
426+
var state = JSON.parse(params.state);
427+
428+
assert.equal(state.arguments.foo, 'bar');
429+
});
430+
});
408431
});
409432

410433
describe('Utilities', function() {

0 commit comments

Comments
 (0)