Skip to content

Commit e5a7db6

Browse files
author
Eric Koleda
committed
Add default behavior for client_credentials grant type.
1 parent 30118e5 commit e5a7db6

File tree

6 files changed

+171
-16
lines changed

6 files changed

+171
-16
lines changed

README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ in your manifest file, ensure that the following scope is included:
3131
## Redirect URI
3232

3333
Before you can start authenticating against an OAuth2 provider, you usually need
34-
to register your application with that OAuth2 provider and obtain a client ID and secret. Often
35-
a provider's registration screen requires you to enter a "Redirect URI", which is the
36-
URL that the user's browser will be redirected to after they've authorized access to their account at that provider.
34+
to register your application with that OAuth2 provider and obtain a client ID
35+
and secret. Often a provider's registration screen requires you to enter a
36+
"Redirect URI", which is the URL that the user's browser will be redirected to
37+
after they've authorized access to their account at that provider.
3738

38-
For this library (and the Apps Script functionality in general) the URL will always
39-
be in the following format:
39+
For this library (and the Apps Script functionality in general) the URL will
40+
always be in the following format:
4041

4142
https://script.google.com/macros/d/{SCRIPT ID}/usercallback
4243

@@ -350,8 +351,15 @@ headers.
350351
351352
The most common of these is the `client_credentials` grant type, which often
352353
requires that the client ID and secret are passed in the Authorization header.
353-
See the sample [`TwitterAppOnly.gs`](samples/TwitterAppOnly.gs) for more
354-
information.
354+
When using this grant type, if you set a client ID and secret using
355+
`setClientId()` and `setClientSecret()` respectively then an
356+
`Authorization: Basic ...` header will be added to the token request
357+
automatically, since this is what most OAuth2 providers require. If your
358+
provider uses a different method of authorization then don't set the client ID
359+
and secret and add an authorization header manually.
360+
361+
See the sample [`TwitterAppOnly.gs`](samples/TwitterAppOnly.gs) for a working
362+
example.
355363
356364
357365
## Compatibility

samples/TwitterAppOnly.gs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,13 @@ function getService() {
4343
// Set the endpoint URLs.
4444
.setTokenUrl('https://api.twitter.com/oauth2/token')
4545

46+
// Set the client ID and secret.
47+
.setClientId(CLIENT_ID)
48+
.setClientSecret(CLIENT_SECRET)
49+
4650
// Sets the custom grant type to use.
4751
.setGrantType('client_credentials')
4852

49-
// Sets the required Authorization header.
50-
.setTokenHeaders({
51-
Authorization: 'Basic ' +
52-
Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET)
53-
})
54-
5553
// Set the property store where authorized tokens should be persisted.
5654
.setPropertyStore(PropertiesService.getUserProperties());
5755
}

src/Service.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -698,8 +698,8 @@ Service_.prototype.lockable_ = function(func) {
698698

699699
/**
700700
* Obtain an access token using the custom grant type specified. Most often
701-
* this will be "client_credentials", in which case make sure to also specify an
702-
* Authorization header if required by your OAuth provider.
701+
* this will be "client_credentials", and a client ID and secret are set an
702+
* "Authorization: Baseic ..." header will be added using thos values.
703703
*/
704704
Service_.prototype.exchangeGrant_ = function() {
705705
validate_({
@@ -710,6 +710,19 @@ Service_.prototype.exchangeGrant_ = function() {
710710
grant_type: this.grantType_
711711
};
712712
payload = extend_(payload, this.params_);
713+
714+
// For the client_credentials grant type, add a basic authorization header
715+
// if the client ID and client secret are set and no authorization header has
716+
// been set yet (AKA do the expected thing).
717+
if (this.grantType_ === 'client_credentials' &&
718+
this.clientId_ &&
719+
this.clientSecret_ &&
720+
!getValueCaseInsensitive_(this.tokenHeaders_, 'Authorization')) {
721+
this.tokenHeaders_ = this.tokenHeaders_ || {};
722+
this.tokenHeaders_.Authorization = 'Basic ' +
723+
Utilities.base64Encode(this.clientId_ + ':' + this.clientSecret_);
724+
}
725+
713726
var token = this.fetchToken_(payload);
714727
this.saveToken_(token);
715728
};

src/Utilities.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,27 @@ function extend_(destination, source) {
7676
}
7777
return destination;
7878
}
79+
80+
/* exported getValueCaseInsensitive_ */
81+
/**
82+
* Gets the value stored in the object under the given key, in a
83+
* case-insensitive way.
84+
* @param {Object} obj The object to search in.
85+
* @param {string} key The key to search for.
86+
* @return {Object} the value under that key, or undefined otherwise
87+
*/
88+
function getValueCaseInsensitive_(obj, key) {
89+
if (obj == null || typeof obj !== 'object' ||
90+
key == null || !key.toString) {
91+
return undefined;
92+
}
93+
if (key in obj) {
94+
return obj[key];
95+
}
96+
return Object.keys(obj).reduce(function(result, k) {
97+
if (result) return result;
98+
if (k.toLowerCase() === key.toLowerCase()) {
99+
return obj[k];
100+
}
101+
}, undefined);
102+
}

test/mocks/urlfetchapp.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var MockUrlFetchApp = function() {
1212

1313
MockUrlFetchApp.prototype.fetch = function(url, optOptions) {
1414
var delay = this.delayFunction();
15-
var result = this.resultFunction();
15+
var result = this.resultFunction(url, optOptions);
1616
if (delay) {
1717
sleep(delay).wait();
1818
}

test/test.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ var mocks = {
1313
}
1414
},
1515
UrlFetchApp: new MockUrlFetchApp(),
16+
Utilities: {
17+
base64Encode: function(data) {
18+
return Buffer.from(data).toString('base64');
19+
}
20+
},
1621
__proto__: gas.globalMockDefault
1722
};
1823
var OAuth2 = gas.require('./src', mocks);
@@ -236,6 +241,82 @@ describe('Service', function() {
236241
});
237242
});
238243
});
244+
245+
describe('#exchangeGrant_()', function() {
246+
var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_;
247+
248+
it('should not set auth header if the grant type is not client_credentials',
249+
function(done) {
250+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
251+
assert.isUndefined(
252+
getValueCaseInsensitive_(urlOptions.headers, 'Authorization'));
253+
done();
254+
};
255+
var service = OAuth2.createService('test')
256+
.setGrantType('fake')
257+
.setTokenUrl('http://www.example.com');
258+
service.exchangeGrant_();
259+
});
260+
261+
it('should not set auth header if the client ID is not set',
262+
function(done) {
263+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
264+
assert.isUndefined(
265+
getValueCaseInsensitive_(urlOptions.headers, 'Authorization'));
266+
done();
267+
};
268+
var service = OAuth2.createService('test')
269+
.setGrantType('client_credentials')
270+
.setTokenUrl('http://www.example.com');
271+
service.exchangeGrant_();
272+
});
273+
274+
it('should not set auth header if the client secret is not set',
275+
function(done) {
276+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
277+
assert.isUndefined(
278+
getValueCaseInsensitive_(urlOptions.headers, 'Authorization'));
279+
done();
280+
};
281+
var service = OAuth2.createService('test')
282+
.setGrantType('client_credentials')
283+
.setTokenUrl('http://www.example.com')
284+
.setClientId('abc');
285+
service.exchangeGrant_();
286+
});
287+
288+
it('should not set auth header if it is already set',
289+
function(done) {
290+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
291+
assert.equal(urlOptions.headers.Authorization, 'something');
292+
done();
293+
};
294+
var service = OAuth2.createService('test')
295+
.setGrantType('client_credentials')
296+
.setTokenUrl('http://www.example.com')
297+
.setClientId('abc')
298+
.setClientSecret('def')
299+
.setTokenHeaders({
300+
Authorization: 'something'
301+
});
302+
service.exchangeGrant_();
303+
});
304+
305+
it('should set the auth header for the client_credentials grant type, if ' +
306+
'the client ID and client secret are set and the authorization header' +
307+
'is not already set', function(done) {
308+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
309+
assert.equal(urlOptions.headers.Authorization, 'Basic YWJjOmRlZg==');
310+
done();
311+
};
312+
var service = OAuth2.createService('test')
313+
.setGrantType('client_credentials')
314+
.setTokenUrl('http://www.example.com')
315+
.setClientId('abc')
316+
.setClientSecret('def');
317+
service.exchangeGrant_();
318+
});
319+
});
239320
});
240321

241322
describe('Utilities', function() {
@@ -255,4 +336,35 @@ describe('Utilities', function() {
255336
assert.deepEqual(o, {foo: [100], bar: 2, baz: {}});
256337
});
257338
});
339+
340+
describe('#getValueCaseInsensitive_()', function() {
341+
var getValueCaseInsensitive_ = OAuth2.getValueCaseInsensitive_;
342+
343+
it('should find identical keys', function() {
344+
assert.isTrue(getValueCaseInsensitive_({'a': true}, 'a'));
345+
assert.isTrue(getValueCaseInsensitive_({'A': true}, 'A'));
346+
assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'Ab'));
347+
});
348+
349+
it('should find matching keys of different cases', function() {
350+
assert.isTrue(getValueCaseInsensitive_({'a': true}, 'A'));
351+
assert.isTrue(getValueCaseInsensitive_({'A': true}, 'a'));
352+
assert.isTrue(getValueCaseInsensitive_({'Ab': true}, 'aB'));
353+
assert.isTrue(getValueCaseInsensitive_({'a2': true}, 'A2'));
354+
});
355+
356+
it('should work with non-alphabetic keys', function() {
357+
assert.isTrue(getValueCaseInsensitive_({'A2': true}, 'a2'));
358+
assert.isTrue(getValueCaseInsensitive_({'2': true}, '2'));
359+
assert.isTrue(getValueCaseInsensitive_({2: true}, 2));
360+
assert.isTrue(getValueCaseInsensitive_({'!@#': true}, '!@#'));
361+
});
362+
363+
it('should work null and undefined', function() {
364+
assert.isUndefined(getValueCaseInsensitive_(null, 'key'));
365+
assert.isUndefined(getValueCaseInsensitive_(undefined, 'key'));
366+
assert.isUndefined(getValueCaseInsensitive_({'a': true}, null));
367+
assert.isUndefined(getValueCaseInsensitive_({'a': true}, undefined));
368+
});
369+
});
258370
});

0 commit comments

Comments
 (0)