Skip to content

Commit bcf34f8

Browse files
author
Eric Koleda
authored
Merge pull request #141 from gsuitedevs/client_credentials
Add default behavior for client_credentials grant type.
2 parents afc5fc6 + d8a5f45 commit bcf34f8

File tree

7 files changed

+168
-22
lines changed

7 files changed

+168
-22
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/Domo.gs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,13 @@ function getService() {
4141
// Set the endpoint URLs.
4242
.setTokenUrl('https://api.domo.com/oauth/token')
4343

44+
// Set the client ID and secret.
45+
.setClientId(CLIENT_ID)
46+
.setClientSecret(CLIENT_SECRET)
47+
4448
// Sets the custom grant type to use.
4549
.setGrantType('client_credentials')
4650

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

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: 16 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: Basic ..." header will be added using those values.
703703
*/
704704
Service_.prototype.exchangeGrant_ = function() {
705705
validate_({
@@ -710,6 +710,20 @@ 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.
716+
// - No authorization header has been set yet.
717+
var lowerCaseHeaders = toLowerCaseKeys_(this.tokenHeaders_);
718+
if (this.grantType_ === 'client_credentials' &&
719+
this.clientId_ &&
720+
this.clientSecret_ &&
721+
(!lowerCaseHeaders || !lowerCaseHeaders.authorization)) {
722+
this.tokenHeaders_ = this.tokenHeaders_ || {};
723+
this.tokenHeaders_.authorization = 'Basic ' +
724+
Utilities.base64Encode(this.clientId_ + ':' + this.clientSecret_);
725+
}
726+
713727
var token = this.fetchToken_(payload);
714728
this.saveToken_(token);
715729
};

src/Utilities.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,22 @@ function extend_(destination, source) {
7676
}
7777
return destination;
7878
}
79+
80+
/* exported toLowerCaseKeys_ */
81+
/**
82+
* Gets a copy of an object with all the keys converted to lower-case strings.
83+
*
84+
* @param {Object} obj The object to copy.
85+
* @return {Object} a shallow copy of the object with all lower-case keys.
86+
*/
87+
function toLowerCaseKeys_(obj) {
88+
if (obj === null || typeof obj !== 'object') {
89+
return obj;
90+
}
91+
// For each key in the source object, add a lower-case version to a new
92+
// object, and return it.
93+
return Object.keys(obj).reduce(function(result, k) {
94+
result[k.toLowerCase()] = obj[k];
95+
return result;
96+
}, {});
97+
}

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: 109 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,81 @@ describe('Service', function() {
236241
});
237242
});
238243
});
244+
245+
describe('#exchangeGrant_()', function() {
246+
var toLowerCaseKeys_ = OAuth2.toLowerCaseKeys_;
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(toLowerCaseKeys_(urlOptions.headers).authorization);
252+
done();
253+
};
254+
var service = OAuth2.createService('test')
255+
.setGrantType('fake')
256+
.setTokenUrl('http://www.example.com');
257+
service.exchangeGrant_();
258+
});
259+
260+
it('should not set auth header if the client ID is not set',
261+
function(done) {
262+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
263+
assert.isUndefined(toLowerCaseKeys_(urlOptions.headers).authorization);
264+
done();
265+
};
266+
var service = OAuth2.createService('test')
267+
.setGrantType('client_credentials')
268+
.setTokenUrl('http://www.example.com');
269+
service.exchangeGrant_();
270+
});
271+
272+
it('should not set auth header if the client secret is not set',
273+
function(done) {
274+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
275+
assert.isUndefined(toLowerCaseKeys_(urlOptions.headers).authorization);
276+
done();
277+
};
278+
var service = OAuth2.createService('test')
279+
.setGrantType('client_credentials')
280+
.setTokenUrl('http://www.example.com')
281+
.setClientId('abc');
282+
service.exchangeGrant_();
283+
});
284+
285+
it('should not set auth header if it is already set',
286+
function(done) {
287+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
288+
assert.equal(toLowerCaseKeys_(urlOptions.headers).authorization,
289+
'something');
290+
done();
291+
};
292+
var service = OAuth2.createService('test')
293+
.setGrantType('client_credentials')
294+
.setTokenUrl('http://www.example.com')
295+
.setClientId('abc')
296+
.setClientSecret('def')
297+
.setTokenHeaders({
298+
authorization: 'something'
299+
});
300+
service.exchangeGrant_();
301+
});
302+
303+
it('should set the auth header for the client_credentials grant type, if ' +
304+
'the client ID and client secret are set and the authorization header' +
305+
'is not already set', function(done) {
306+
mocks.UrlFetchApp.resultFunction = function(url, urlOptions) {
307+
assert.equal(toLowerCaseKeys_(urlOptions.headers).authorization,
308+
'Basic YWJjOmRlZg==');
309+
done();
310+
};
311+
var service = OAuth2.createService('test')
312+
.setGrantType('client_credentials')
313+
.setTokenUrl('http://www.example.com')
314+
.setClientId('abc')
315+
.setClientSecret('def');
316+
service.exchangeGrant_();
317+
});
318+
});
239319
});
240320

241321
describe('Utilities', function() {
@@ -255,4 +335,33 @@ describe('Utilities', function() {
255335
assert.deepEqual(o, {foo: [100], bar: 2, baz: {}});
256336
});
257337
});
338+
339+
describe('#toLowerCaseKeys_()', function() {
340+
var toLowerCaseKeys_ = OAuth2.toLowerCaseKeys_;
341+
342+
it('should contain only lower-case keys', function() {
343+
var data = {
344+
'a': true,
345+
'A': true,
346+
'B': true,
347+
'Cc': true,
348+
'D2': true,
349+
'E!@#': true
350+
};
351+
var lowerCaseData = toLowerCaseKeys_(data);
352+
assert.deepEqual(lowerCaseData, {
353+
'a': true,
354+
'b': true,
355+
'cc': true,
356+
'd2': true,
357+
'e!@#': true
358+
});
359+
});
360+
361+
it('should handle null, undefined, and empty objects', function() {
362+
assert.isNull(toLowerCaseKeys_(null));
363+
assert.isUndefined(toLowerCaseKeys_(undefined));
364+
assert.isEmpty(toLowerCaseKeys_({}));
365+
});
366+
});
258367
});

0 commit comments

Comments
 (0)