Skip to content

Commit 7b989ca

Browse files
author
Eric Koleda
authored
Merge pull request #133 from gsuitedevs/client_credentials
Add support for the client_credentials flow
2 parents 30cfe6d + 814d239 commit 7b989ca

File tree

4 files changed

+202
-62
lines changed

4 files changed

+202
-62
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,20 @@ authorization flow to obtain an access token. See the sample
340340
[`GoogleServiceAccount.gs`](samples/GoogleServiceAccount.gs) for more
341341
information.
342342
343+
#### Using alternative grant types
344+
345+
Although optimized for the authorization code (3-legged) and service account
346+
(JWT bearer) flows, this library supports arbitrary flows using the
347+
`setGrantType()` method. Use `setParam()` or `setTokenPayloadHandler()` to add
348+
fields to the token request payload, and `setTokenHeaders()` to add any required
349+
headers.
350+
351+
The most common of these is the `client_credentials` grant type, which often
352+
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.
355+
356+
343357
## Compatibility
344358
345359
This library was designed to work with any OAuth2 provider, but because of small

samples/Domo.gs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* This sample demonstrates how to configure the library for the Domo API.
3+
* Instructions on how to generate OAuth credentuals is available here:
4+
* https://developer.domo.com/docs/authentication/overview-4
5+
*/
6+
7+
var CLIENT_ID = '...';
8+
var CLIENT_SECRET = '...';
9+
10+
/**
11+
* Authorizes and makes a request to the Domo API.
12+
*/
13+
function run() {
14+
var service = getService();
15+
if (service.hasAccess()) {
16+
var url = 'https://api.domo.com/v1/users';
17+
var response = UrlFetchApp.fetch(url, {
18+
headers: {
19+
Authorization: 'Bearer ' + service.getAccessToken()
20+
}
21+
});
22+
var result = JSON.parse(response.getContentText());
23+
Logger.log(JSON.stringify(result, null, 2));
24+
} else {
25+
Logger.log(service.getLastError());
26+
}
27+
}
28+
29+
/**
30+
* Reset the authorization state, so that it can be re-tested.
31+
*/
32+
function reset() {
33+
getService().reset();
34+
}
35+
36+
/**
37+
* Configures the service.
38+
*/
39+
function getService() {
40+
return OAuth2.createService('Domo')
41+
// Set the endpoint URLs.
42+
.setTokenUrl('https://api.domo.com/oauth/token')
43+
44+
// Sets the custom grant type to use.
45+
.setGrantType('client_credentials')
46+
47+
// Sets the required Authorization header.
48+
.setTokenHeaders({
49+
Authorization: 'Basic ' +
50+
Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET)
51+
})
52+
53+
// Set the property store where authorized tokens should be persisted.
54+
.setPropertyStore(PropertiesService.getUserProperties());
55+
}

samples/TwitterAppOnly.gs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* This sample demonstrates how to configure the library for the Twitter API,
3+
* using the Application Only authorization flow:
4+
* https://developer.twitter.com/en/docs/basics/authentication/overview/application-only
5+
* To authorize access to a user's Twitter account you need to use the OAuth
6+
* 1.0a library as shown here:
7+
* https://developer.twitter.com/en/docs/basics/authentication/overview/application-only
8+
*/
9+
var CLIENT_ID = '...';
10+
var CLIENT_SECRET = '...';
11+
12+
/**
13+
* Authorizes and makes a request to the Twitter API.
14+
*/
15+
function run() {
16+
var service = getService();
17+
if (service.hasAccess()) {
18+
var url = 'https://api.twitter.com/1.1/users/show.json?screen_name=gsuitedevs';
19+
var response = UrlFetchApp.fetch(url, {
20+
headers: {
21+
Authorization: 'Bearer ' + service.getAccessToken()
22+
}
23+
});
24+
var result = JSON.parse(response.getContentText());
25+
Logger.log(JSON.stringify(result, null, 2));
26+
} else {
27+
Logger.log(service.getLastError());
28+
}
29+
}
30+
31+
/**
32+
* Reset the authorization state, so that it can be re-tested.
33+
*/
34+
function reset() {
35+
getService().reset();
36+
}
37+
38+
/**
39+
* Configures the service.
40+
*/
41+
function getService() {
42+
return OAuth2.createService('Twitter App Only')
43+
// Set the endpoint URLs.
44+
.setTokenUrl('https://api.twitter.com/oauth2/token')
45+
46+
// Sets the custom grant type to use.
47+
.setGrantType('client_credentials')
48+
49+
// Sets the required Authorization header.
50+
.setTokenHeaders({
51+
Authorization: 'Basic ' +
52+
Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET)
53+
})
54+
55+
// Set the property store where authorized tokens should be persisted.
56+
.setPropertyStore(PropertiesService.getUserProperties());
57+
}

src/Service.js

Lines changed: 76 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,20 @@ Service_.prototype.setExpirationMinutes = function(expirationMinutes) {
287287
return this;
288288
};
289289

290+
/**
291+
* Sets the OAuth2 grant_type to use when obtaining an access token. This does
292+
* not need to be set when using either the authorization code flow (AKA
293+
* 3-legged OAuth) or the service account flow. The most common usage is to set
294+
* it to "client_credentials" and then also set the token headers to include
295+
* the Authorization header required by the OAuth2 provider.
296+
* @param {string} grantType The OAuth2 grant_type value.
297+
* @return {Service_} This service, for chaining.
298+
*/
299+
Service_.prototype.setGrantType = function(grantType) {
300+
this.grantType_ = grantType;
301+
return this;
302+
};
303+
290304
/**
291305
* Gets the authorization URL. The first step in getting an OAuth2 token is to
292306
* have the user visit this URL and approve the authorization request. The
@@ -342,29 +356,14 @@ Service_.prototype.handleCallback = function(callbackRequest) {
342356
'Token URL': this.tokenUrl_
343357
});
344358
var redirectUri = getRedirectUri(this.scriptId_);
345-
var headers = {
346-
'Accept': this.tokenFormat_
347-
};
348-
if (this.tokenHeaders_) {
349-
headers = extend_(headers, this.tokenHeaders_);
350-
}
351-
var tokenPayload = {
359+
var payload = {
352360
code: code,
353361
client_id: this.clientId_,
354362
client_secret: this.clientSecret_,
355363
redirect_uri: redirectUri,
356364
grant_type: 'authorization_code'
357365
};
358-
if (this.tokenPayloadHandler_) {
359-
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
360-
}
361-
var response = UrlFetchApp.fetch(this.tokenUrl_, {
362-
method: 'post',
363-
headers: headers,
364-
payload: tokenPayload,
365-
muteHttpExceptions: true
366-
});
367-
var token = this.getTokenFromResponse_(response);
366+
var token = this.fetchToken_(payload);
368367
this.saveToken_(token);
369368
return true;
370369
};
@@ -380,21 +379,18 @@ Service_.prototype.hasAccess = function() {
380379
return this.lockable_(function() {
381380
var token = this.getToken();
382381
if (!token || this.isExpired_(token)) {
383-
if (token && this.canRefresh_(token)) {
384-
try {
382+
try {
383+
if (token && this.canRefresh_(token)) {
385384
this.refresh();
386-
} catch (e) {
387-
this.lastError_ = e;
388-
return false;
389-
}
390-
} else if (this.privateKey_) {
391-
try {
385+
} else if (this.privateKey_) {
392386
this.exchangeJwt_();
393-
} catch (e) {
394-
this.lastError_ = e;
387+
} else if (this.grantType_) {
388+
this.exchangeGrant_();
389+
} else {
395390
return false;
396391
}
397-
} else {
392+
} catch (e) {
393+
this.lastError_ = e;
398394
return false;
399395
}
400396
}
@@ -442,6 +438,34 @@ Service_.prototype.getRedirectUri = function() {
442438
return getRedirectUri(this.scriptId_);
443439
};
444440

441+
442+
/**
443+
* Fetches a new token from the OAuth server.
444+
* @param {Object} payload The token request payload.
445+
* @param {string} [optUrl] The URL of the token endpoint.
446+
* @return {Object} The parsed token.
447+
*/
448+
Service_.prototype.fetchToken_ = function(payload, optUrl) {
449+
// Use the configured token URL unless one is specified.
450+
var url = optUrl || this.tokenUrl_;
451+
var headers = {
452+
'Accept': this.tokenFormat_
453+
};
454+
if (this.tokenHeaders_) {
455+
headers = extend_(headers, this.tokenHeaders_);
456+
}
457+
if (this.tokenPayloadHandler_) {
458+
tokenPayload = this.tokenPayloadHandler_(payload);
459+
}
460+
var response = UrlFetchApp.fetch(url, {
461+
method: 'post',
462+
headers: headers,
463+
payload: payload,
464+
muteHttpExceptions: true
465+
});
466+
return this.getTokenFromResponse_(response);
467+
};
468+
445469
/**
446470
* Gets the token from a UrlFetchApp response.
447471
* @param {UrlFetchApp.HTTPResponse} response The response object.
@@ -512,30 +536,13 @@ Service_.prototype.refresh = function() {
512536
if (!token.refresh_token) {
513537
throw new Error('Offline access is required.');
514538
}
515-
var headers = {
516-
Accept: this.tokenFormat_
517-
};
518-
if (this.tokenHeaders_) {
519-
headers = extend_(headers, this.tokenHeaders_);
520-
}
521-
var tokenPayload = {
539+
var payload = {
522540
refresh_token: token.refresh_token,
523541
client_id: this.clientId_,
524542
client_secret: this.clientSecret_,
525543
grant_type: 'refresh_token'
526544
};
527-
if (this.tokenPayloadHandler_) {
528-
tokenPayload = this.tokenPayloadHandler_(tokenPayload);
529-
}
530-
// Use the refresh URL if specified, otherwise fallback to the token URL.
531-
var url = this.refreshUrl_ || this.tokenUrl_;
532-
var response = UrlFetchApp.fetch(url, {
533-
method: 'post',
534-
headers: headers,
535-
payload: tokenPayload,
536-
muteHttpExceptions: true
537-
});
538-
var newToken = this.getTokenFromResponse_(response);
545+
var newToken = this.fetchToken_(payload, this.refreshUrl_);
539546
if (!newToken.refresh_token) {
540547
newToken.refresh_token = token.refresh_token;
541548
}
@@ -623,22 +630,11 @@ Service_.prototype.exchangeJwt_ = function() {
623630
'Token URL': this.tokenUrl_
624631
});
625632
var jwt = this.createJwt_();
626-
var headers = {
627-
'Accept': this.tokenFormat_
633+
var payload = {
634+
assertion: jwt,
635+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
628636
};
629-
if (this.tokenHeaders_) {
630-
headers = extend_(headers, this.tokenHeaders_);
631-
}
632-
var response = UrlFetchApp.fetch(this.tokenUrl_, {
633-
method: 'post',
634-
headers: headers,
635-
payload: {
636-
assertion: jwt,
637-
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer'
638-
},
639-
muteHttpExceptions: true
640-
});
641-
var token = this.getTokenFromResponse_(response);
637+
var token = this.fetchToken_(payload);
642638
this.saveToken_(token);
643639
};
644640

@@ -699,3 +695,21 @@ Service_.prototype.lockable_ = function(func) {
699695
}
700696
return result;
701697
};
698+
699+
/**
700+
* 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.
703+
*/
704+
Service_.prototype.exchangeGrant_ = function() {
705+
validate_({
706+
'Grant Type': this.grantType_,
707+
'Token URL': this.tokenUrl_
708+
});
709+
var payload = {
710+
grant_type: this.grantType_
711+
};
712+
payload = extend_(payload, this.params_);
713+
var token = this.fetchToken_(payload);
714+
this.saveToken_(token);
715+
};

0 commit comments

Comments
 (0)