diff --git a/dist/OAuth2.gs b/dist/OAuth2.gs index adf3bb84..ce97757c 100644 --- a/dist/OAuth2.gs +++ b/dist/OAuth2.gs @@ -759,7 +759,6 @@ Service_.prototype.ensureExpiresAtSet_ = function(token) { Service_.prototype.refresh = function() { validate_({ 'Client ID': this.clientId_, - 'Client Secret': this.clientSecret_, 'Token URL': this.tokenUrl_ }); diff --git a/samples/Basecamp.gs b/samples/Basecamp.gs index cbdc66d0..d7f45023 100644 --- a/samples/Basecamp.gs +++ b/samples/Basecamp.gs @@ -35,10 +35,11 @@ function reset() { function getService_() { return OAuth2.createService('Basecamp') // Set the endpoint URLs. - .setAuthorizationBaseUrl( - 'https://launchpad.37signals.com/authorization/new?type=web_server') - .setTokenUrl( - 'https://launchpad.37signals.com/authorization/token?type=web_server') + .setAuthorizationBaseUrl('https://launchpad.37signals.com/authorization/new') + .setTokenUrl('https://launchpad.37signals.com/authorization/token') + + // Set the required type param + .setParam('type', 'web_server') // Set the client ID and secret. .setClientId(CLIENT_ID) @@ -49,7 +50,11 @@ function getService_() { .setCallbackFunction('authCallback') // Set the property store where authorized tokens should be persisted. - .setPropertyStore(PropertiesService.getUserProperties()); + .setPropertyStore(PropertiesService.getUserProperties()) + + // Set the handler for adding Basecamp's required type parameter to the + // payload: + .setTokenPayloadHandler(basecampTokenHandler); } /** @@ -65,6 +70,23 @@ function authCallback(request) { } } +/** + * Adds the Basecamp API's required type parameter to the access token + * request payload. + */ +function basecampTokenHandler(payload) { + // If it's refresh request from library + if (payload.grant_type === 'refresh_token') { + // Basecamp refresh token API returns error if type is not specified + payload.type = 'refresh'; + } else { + // Basecamp token API returns error if type is not specified + payload.type = 'web_server'; + } + + return payload; +} + /** * Logs the redict URI to register. */ diff --git a/samples/Dropbox.gs b/samples/Dropbox.gs index 79826b10..b29ce884 100644 --- a/samples/Dropbox.gs +++ b/samples/Dropbox.gs @@ -53,8 +53,8 @@ function getService_() { // Set the property store where authorized tokens should be persisted. .setPropertyStore(PropertiesService.getUserProperties()) - // Set the response type to code (required). - .setParam('response_type', 'code'); + // Enable offline access (refresh_token). + .setParam('token_access_type', 'offline'); } /** diff --git a/samples/Twitter.gs b/samples/Twitter.gs index d149882d..3c3ed32f 100644 --- a/samples/Twitter.gs +++ b/samples/Twitter.gs @@ -31,21 +31,17 @@ function run() { */ function reset() { getService_().reset(); - PropertiesService.getUserProperties().deleteProperty('code_challenge'); - PropertiesService.getUserProperties().deleteProperty('code_verifier'); } /** * Configures the service. */ function getService_() { - pkceChallengeVerifier(); var userProps = PropertiesService.getUserProperties(); return OAuth2.createService('Twitter') // Set the endpoint URLs. .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize') - .setTokenUrl( - 'https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty('code_verifier')) + .setTokenUrl('https://api.twitter.com/2/oauth2/token') // Set the client ID and secret. .setClientId(CLIENT_ID) @@ -61,10 +57,8 @@ function getService_() { // Set the scopes to request (space-separated for Twitter services). .setScope('users.read tweet.read offline.access') - // Add parameters in the authorization url - .setParam('response_type', 'code') - .setParam('code_challenge_method', 'S256') - .setParam('code_challenge', userProps.getProperty('code_challenge')) + // Generate code verifier parameter + .generateCodeVerifier() .setTokenHeaders({ 'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET), @@ -91,27 +85,3 @@ function authCallback(request) { function logRedirectUri() { Logger.log(OAuth2.getRedirectUri()); } - -/** - * Generates code_verifier & code_challenge for PKCE - */ -function pkceChallengeVerifier() { - var userProps = PropertiesService.getUserProperties(); - if (!userProps.getProperty('code_verifier')) { - var verifier = ''; - var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; - - for (var i = 0; i < 128; i++) { - verifier += possible.charAt(Math.floor(Math.random() * possible.length)); - } - - var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier); - - var challenge = Utilities.base64Encode(sha256Hash) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=+$/, ''); - userProps.setProperty('code_verifier', verifier); - userProps.setProperty('code_challenge', challenge); - } -} diff --git a/src/Service.js b/src/Service.js index 68bd3e7f..e7527fe1 100644 --- a/src/Service.js +++ b/src/Service.js @@ -666,7 +666,6 @@ Service_.prototype.ensureExpiresAtSet_ = function(token) { Service_.prototype.refresh = function() { validate_({ 'Client ID': this.clientId_, - 'Client Secret': this.clientSecret_, 'Token URL': this.tokenUrl_ }); diff --git a/test/test.js b/test/test.js index 1e8317f7..fe7aca71 100644 --- a/test/test.js +++ b/test/test.js @@ -387,6 +387,35 @@ describe('Service', () => { done(); }); + it('should refresh token granted for PKCE', () => { + const NOW_SECONDS = OAuth2.getTimeInSeconds_(new Date()); + const ONE_HOUR_AGO_SECONDS = NOW_SECONDS - 360; + var token = { + granted_time: ONE_HOUR_AGO_SECONDS, + expires_in: 100, + refresh_token: 'bar', + refresh_token_expires_in: 720 + }; + var properties = new MockProperties({ + 'oauth2.test': JSON.stringify(token) + }); + + mocks.UrlFetchApp.resultFunction = () => JSON.stringify({ + access_token: Math.random().toString(36) + }); + + OAuth2.createService('test') + .setClientId('test') + .setTokenUrl('http://www.example.com/') + .setPropertyStore(properties) + .generateCodeVerifier() + .refresh(); + + var storedToken = JSON.parse(properties.getProperty('oauth2.test')); + assert.equal(storedToken.refresh_token, 'bar'); + assert.equal(storedToken.refreshTokenExpiresAt, NOW_SECONDS + 360); + }); + it('should retain refresh expiry', () => { const NOW_SECONDS = OAuth2.getTimeInSeconds_(new Date()); const ONE_HOUR_AGO_SECONDS = NOW_SECONDS - 360;