From d7f5dbf1959b47873c78599cb14c85c73cac84ab Mon Sep 17 00:00:00 2001 From: Mark Dawson Date: Tue, 17 Dec 2013 22:26:06 -0800 Subject: [PATCH] Adding support for salesforce.com - Adds support for login.salesforce.com and test.salesforce.com oauth - Fixed a missing error callback - Added salesforce sample --- .gitignore | 2 + README.md | 22 ++++++++++- lib/adapters/salesforce.js | 60 ++++++++++++++++++++++++++++++ lib/adapters/salesforce_test.js | 60 ++++++++++++++++++++++++++++++ lib/oauth2.html | 2 +- lib/oauth2.js | 65 +++++++++++++++++++++++++++++---- samples/mixed/manifest.json | 12 +++++- samples/mixed/options.html | 6 +++ samples/mixed/options.js | 22 +++++++++-- 9 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 .gitignore create mode 100644 lib/adapters/salesforce.js create mode 100644 lib/adapters/salesforce_test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d19e1d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +samples/*/oauth2 \ No newline at end of file diff --git a/README.md b/README.md index cbba3ab..f414970 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,16 @@ Here's a table that will come in handy: http://www.feedly.com/robots.txt https://cloud.feedly.com/v3/auth/token + + salesforce + https://login.salesforce.com/services/oauth2/success + https://login.salesforce.com/services/oauth2/token + + + salesforce_test + https://test.salesforce.com/services/oauth2/success + https://test.salesforce.com/services/oauth2/token + #### Step 1: Copy library @@ -108,7 +118,17 @@ extension access to the OAuth2 endpoint. api_scope: 'https://www.googleapis.com/auth/tasks' }); - googleAuth.authorize(function() { + googleAuth.authorize(function(error, details) { + if (error) { + // Handle error + return; + } + + // The details object is the result of the call to the + // adapters parseAccessToken function - there may be extra + // information returned from the server, e.g. the salesforce + // instanceUrl value when authing with salesforce + // Ready for action, can now make requests with googleAuth.getAccessToken() }); diff --git a/lib/adapters/salesforce.js b/lib/adapters/salesforce.js new file mode 100644 index 0000000..d392b4b --- /dev/null +++ b/lib/adapters/salesforce.js @@ -0,0 +1,60 @@ +OAuth2.adapter('salesforce', { + + authorizationCodeURL: function(config) { + return ('https://login.salesforce.com/services/oauth2/authorize?' + + 'response_type=code&' + + 'client_id={{CLIENT_ID}}&' + + 'scope={{API_SCOPE}}&' + + 'display=touch&' + + 'redirect_uri={{REDIRECT_URI}}') + .replace('{{CLIENT_ID}}', config.clientId) + .replace('{{API_SCOPE}}', config.apiScope) + .replace('{{REDIRECT_URI}}', this.redirectURL(config)); + }, + + redirectURL: function(config) { + return 'https://login.salesforce.com/services/oauth2/success'; + }, + + parseAuthorizationCode: function(url) { + var error = url.match(/[&\?]error=([^&]+)/); + if (error) { + throw 'Error getting authorization code: ' + error[1]; + } + + url = decodeURIComponent(url); + return url.match(/[&\?]code=([\w\/\-\=\.]+)/)[1]; + }, + + accessTokenURL: function() { + return 'https://login.salesforce.com/services/oauth2/token'; + }, + + accessTokenMethod: function() { + return 'POST'; + }, + + accessTokenParams: function(authorizationCode, config) { + return { + code: authorizationCode, + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: this.redirectURL(config), + grant_type: 'authorization_code' + }; + }, + + parseAccessToken: function(response) { + var values = JSON.parse(response); + return { + accessToken: values.access_token, + refreshToken: values.refresh_token, + + // We don't get this info in the response from the salesforce + // server, the value depends on what the admin of the org set + expiresIn: null, + + instanceUrl: values.instance_url + }; + } +}); diff --git a/lib/adapters/salesforce_test.js b/lib/adapters/salesforce_test.js new file mode 100644 index 0000000..6ee8a15 --- /dev/null +++ b/lib/adapters/salesforce_test.js @@ -0,0 +1,60 @@ +OAuth2.adapter('salesforce_test', { + + authorizationCodeURL: function(config) { + return ('https://test.salesforce.com/services/oauth2/authorize?' + + 'response_type=code&' + + 'client_id={{CLIENT_ID}}&' + + 'scope={{API_SCOPE}}&' + + 'display=touch&' + + 'redirect_uri={{REDIRECT_URI}}') + .replace('{{CLIENT_ID}}', config.clientId) + .replace('{{API_SCOPE}}', config.apiScope) + .replace('{{REDIRECT_URI}}', this.redirectURL(config)); + }, + + redirectURL: function(config) { + return 'https://test.salesforce.com/services/oauth2/success'; + }, + + parseAuthorizationCode: function(url) { + var error = url.match(/[&\?]error=([^&]+)/); + if (error) { + throw 'Error getting authorization code: ' + error[1]; + } + + url = decodeURIComponent(url); + return url.match(/[&\?]code=([\w\/\-\=\.]+)/)[1]; + }, + + accessTokenURL: function() { + return 'https://test.salesforce.com/services/oauth2/token'; + }, + + accessTokenMethod: function() { + return 'POST'; + }, + + accessTokenParams: function(authorizationCode, config) { + return { + code: authorizationCode, + client_id: config.clientId, + client_secret: config.clientSecret, + redirect_uri: this.redirectURL(config), + grant_type: 'authorization_code' + }; + }, + + parseAccessToken: function(response) { + var values = JSON.parse(response); + return { + accessToken: values.access_token, + refreshToken: values.refresh_token, + + // We don't get this info in the response from the salesforce + // server, the value depends on what the admin of the org set + expiresIn: null, + + instanceUrl: values.instance_url + }; + } +}); diff --git a/lib/oauth2.html b/lib/oauth2.html index e305a7a..7d1d733 100644 --- a/lib/oauth2.html +++ b/lib/oauth2.html @@ -20,4 +20,4 @@ - \ No newline at end of file + diff --git a/lib/oauth2.js b/lib/oauth2.js index 6020e0b..6e4680f 100644 --- a/lib/oauth2.js +++ b/lib/oauth2.js @@ -119,8 +119,10 @@ OAuth2.prototype.openAuthorizationCodePopup = function(callback) { * @param {String} authorizationCode Retrieved from the first step in the process * @param {Function} callback Called back with 3 params: * access token, refresh token and expiry time + * @param {Function} errorCallback Called with one parameter, the error. The error object contains + * responseText, status and statusText from the xhr object */ -OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callback) { +OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callback, errorCallback) { var that = this; // Make an XHR to get the token var xhr = new XMLHttpRequest(); @@ -130,6 +132,15 @@ OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callbac // Callback with the data (incl. tokens). callback(that.adapter.parseAccessToken(xhr.responseText)); } + else { + if (!errorCallback) return; + + errorCallback({ + responseText: xhr.responseText, + status: xhr.status, + statusText: xhr.statusText + }); + } } }); @@ -141,6 +152,7 @@ OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callbac for (key in items) { formData.append(key, items[key]); } + xhr.open(method, that.adapter.accessTokenURL(), true); xhr.send(formData); } else if (method == 'GET') { @@ -159,8 +171,6 @@ OAuth2.prototype.getAccessAndRefreshTokens = function(authorizationCode, callbac /** * Refreshes the access token using the currently stored refresh token - * Note: this only happens for the Google adapter since all other OAuth 2.0 - * endpoints don't implement refresh tokens. * * @param {String} refreshToken A valid refresh token * @param {Function} callback On success, called with access token and expiry time and refresh token @@ -198,11 +208,11 @@ OAuth2.prototype.finishAuth = function() { var that = this; // Loop through existing extension views and excute any stored callbacks. - function callback(error) { + function callback(error, details) { var views = chrome.extension.getViews(); for (var i = 0, view; view = views[i]; i++) { if (view['oauth-callback']) { - view['oauth-callback'](error); + view['oauth-callback'](error, details); // TODO: Decide whether it's worth it to scope the callback or not. // Currently, every provider will share the same callback address but // that's not such a big deal assuming that they check to see whether @@ -236,15 +246,23 @@ OAuth2.prototype.finishAuth = function() { } that.setSource(data); - callback(); + callback(null, response); + }, function(error) { + callback(error); }); }; /** - * @return True iff the current access token has expired + * @return True if the current access token has expired */ OAuth2.prototype.isAccessTokenExpired = function() { var data = this.get(); + + // If we don't have the expiresIn value, then assume the access + // token has expired + if (!data.expiresIn) { + return true; + } return (new Date().valueOf() - data.accessTokenDate) > data.expiresIn * 1000; }; @@ -460,8 +478,39 @@ OAuth2.prototype.hasAccessToken = function() { }; /** - * Clears an access token, effectively "logging out" of the service. + * Clears an access token. */ OAuth2.prototype.clearAccessToken = function() { this.clear('accessToken'); }; + +/** + * @returns A valid refresh token + */ +OAuth2.prototype.getRefreshToken = function() { + return this.get('refreshToken'); +}; + +/** + * Indicate whether or not a valid refresh token exists + * + * @returns {Boolean} True if a refresh token exists; otherwise false; + */ +OAuth2.prototype.hasRefreshToken = function() { + return !!this.get('refreshToken'); +}; + +/** + * Clears a refresh token + */ +OAuth2.prototype.clearRefreshToken = function() { + this.clear('refreshToken'); +}; + +/** + * Clears all stored tokens, effectively logging out of the service + */ +OAuth2.prototype.clearTokens = function() { + this.clearAccessToken(); + this.clearRefreshToken(); +}; \ No newline at end of file diff --git a/samples/mixed/manifest.json b/samples/mixed/manifest.json index e613388..4db00cb 100644 --- a/samples/mixed/manifest.json +++ b/samples/mixed/manifest.json @@ -27,12 +27,22 @@ "matches": ["https://github.com/robots.txt*"], "js": ["oauth2/oauth2_inject.js"], "run_at": "document_start" + }, + { + "matches": [ + "https://test.salesforce.com/services/oauth2/success*", + "https://login.salesforce.com/services/oauth2/success*" + ], + "js": ["oauth2/oauth2_inject.js"], + "run_at": "document_start" } ], "permissions": [ "https://graph.facebook.com/", "https://accounts.google.com/o/oauth2/token", - "https://github.com/" + "https://github.com/", + "https://test.salesforce.com/services/oauth2/token", + "https://login.salesforce.com/services/oauth2/token" ], "web_accessible_resources" : [ "oauth2/oauth2.html" diff --git a/samples/mixed/options.html b/samples/mixed/options.html index 16ffc43..90b1e17 100644 --- a/samples/mixed/options.html +++ b/samples/mixed/options.html @@ -37,5 +37,11 @@

OAuth 2.0 Permissions

+ + diff --git a/samples/mixed/options.js b/samples/mixed/options.js index 3508056..bdbad9d 100644 --- a/samples/mixed/options.js +++ b/samples/mixed/options.js @@ -12,7 +12,19 @@ var github = new OAuth2('github', { client_id: '09450dfdc3ae76768b08', - client_secret: '8ecfc23e0dba1ce1a295fbabc01fa71db4b80261', + client_secret: '8ecfc23e0dba1ce1a295fbabc01fa71db4b80261' + }); + + var salesforce_test = new OAuth2('salesforce_test', { + client_id: '9MVG982oBBDdwyHjmSlfqa7kDEdYzTMk_07sSeJjETiIWQhnUa_RuV32Te.jt9aP0g8wOB3BOqRBqDJr0m5Cm', + client_secret: '3113638331195393628', + api_scope: '' + }); + + var salesforce = new OAuth2('salesforce', { + client_id: '9MVG982oBBDdwyHjmSlfqa7kDEdYzTMk_07sSeJjETiIWQhnUa_RuV32Te.jt9aP0g8wOB3BOqRBqDJr0m5Cm', + client_secret: '3113638331195393628', + api_scope: '' }); function authorize(providerName) { @@ -22,7 +34,7 @@ function clearAuthorized() { console.log('clear'); - ['google', 'facebook', 'github'].forEach(function(providerName) { + ['google', 'facebook', 'github', 'salesforce', 'salesforce_test'].forEach(function(providerName) { var provider = window[providerName]; provider.clearAccessToken(); }); @@ -31,7 +43,7 @@ function checkAuthorized() { console.log('checkAuthorized'); - ['google', 'facebook', 'github'].forEach(function(providerName) { + ['google', 'facebook', 'github', 'salesforce', 'salesforce_test'].forEach(function(providerName) { var provider = window[providerName]; var button = document.querySelector('#' + providerName); if (provider.hasAccessToken()) { @@ -46,7 +58,9 @@ document.addEventListener('DOMContentLoaded', function () { document.querySelector('button#google').addEventListener('click', function() { authorize('google'); }); document.querySelector('button#github').addEventListener('click', function() { authorize('github'); }); document.querySelector('button#facebook').addEventListener('click', function() { authorize('facebook'); }); - document.querySelector('button#clear').addEventListener('click', function() { clearAuthorized() }); + document.querySelector('button#salesforce').addEventListener('click', function() { authorize('salesforce'); }); + document.querySelector('button#salesforce_test').addEventListener('click', function() { authorize('salesforce_test'); }); + document.querySelector('button#clear').addEventListener('click', function() { clearAuthorized(); }); checkAuthorized(); });