From 03b77061342373f683097cca5e4e7d1f626a891e Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 18:37:29 -0400 Subject: [PATCH 01/38] first pass at oauth1 with twitter --- .../accounts-oauth1-helper/oauth1_client.js | 62 ++++++++ .../accounts-oauth1-helper/oauth1_common.js | 1 + .../accounts-oauth1-helper/oauth1_server.js | 150 ++++++++++++++++++ .../accounts-oauth1-helper/oauth1_tests.js | 39 +++++ packages/accounts-oauth1-helper/package.js | 19 +++ .../accounts-oauth2-helper/oauth2_server.js | 2 +- packages/accounts-twitter/package.js | 13 ++ packages/accounts-twitter/twitter_client.js | 13 ++ packages/accounts-twitter/twitter_common.js | 16 ++ packages/accounts-twitter/twitter_server.js | 20 +++ packages/accounts-ui/login_buttons.js | 15 ++ packages/accounts-ui/login_buttons_images.css | 6 + packages/oauth1/oauth1.js | 122 ++++++++++++++ packages/oauth1/package.js | 11 ++ 14 files changed, 488 insertions(+), 1 deletion(-) create mode 100644 packages/accounts-oauth1-helper/oauth1_client.js create mode 100644 packages/accounts-oauth1-helper/oauth1_common.js create mode 100644 packages/accounts-oauth1-helper/oauth1_server.js create mode 100644 packages/accounts-oauth1-helper/oauth1_tests.js create mode 100644 packages/accounts-oauth1-helper/package.js create mode 100644 packages/accounts-twitter/package.js create mode 100644 packages/accounts-twitter/twitter_client.js create mode 100644 packages/accounts-twitter/twitter_common.js create mode 100644 packages/accounts-twitter/twitter_server.js create mode 100644 packages/oauth1/oauth1.js create mode 100644 packages/oauth1/package.js diff --git a/packages/accounts-oauth1-helper/oauth1_client.js b/packages/accounts-oauth1-helper/oauth1_client.js new file mode 100644 index 0000000000..f859ff69cf --- /dev/null +++ b/packages/accounts-oauth1-helper/oauth1_client.js @@ -0,0 +1,62 @@ +(function () { + // Open a popup window pointing to a OAuth1 handshake page + // + // @param state {String} The OAuth1 state generated by the client + // @param url {String} url to page + Meteor.accounts.oauth1.initiateLogin = function(state, url) { + // XXX these dimensions worked well for facebook and google, but + // it's sort of weird to have these here. Maybe an optional + // argument instead? + var popup = openCenteredPopup(url, 650, 331); + + var checkPopupOpen = setInterval(function() { + if (popup.closed) { + clearInterval(checkPopupOpen); + tryLoginAfterPopupClosed(state); + } + }, 100); + }; + + // Send an OAuth login method to the server. If the user authorized + // access in the popup this should log the user in, otherwise + // nothing should happen. + var tryLoginAfterPopupClosed = function(state) { + Meteor.apply('login', [ + {oauth: {version: 1, state: state}} + ], {wait: true}, function(error, result) { + if (error) + throw error; + + if (!result) { + // The user either closed the OAuth popup or didn't authorize + // access. Do nothing. + return; + } else { + Meteor.accounts.makeClientLoggedIn(result.id, result.token); + } + }); + }; + + var openCenteredPopup = function(url, width, height) { + var screenX = typeof window.screenX !== 'undefined' + ? window.screenX : window.screenLeft; + var screenY = typeof window.screenY !== 'undefined' + ? window.screenY : window.screenTop; + var outerWidth = typeof window.outerWidth !== 'undefined' + ? window.outerWidth : document.body.clientWidth; + var outerHeight = typeof window.outerHeight !== 'undefined' + ? window.outerHeight : (document.body.clientHeight - 22); + + // Use `outerWidth - width` and `outerHeight - height` for help in + // positioning the popup centered relative to the current window + var left = screenX + (outerWidth - width) / 2; + var top = screenY + (outerHeight - height) / 2; + var features = ('width=' + width + ',height=' + height + + ',left=' + left + ',top=' + top); + + var newwindow = window.open(url, 'Login', features); + if (newwindow.focus) + newwindow.focus(); + return newwindow; + }; +})(); \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_common.js b/packages/accounts-oauth1-helper/oauth1_common.js new file mode 100644 index 0000000000..3b746c7e43 --- /dev/null +++ b/packages/accounts-oauth1-helper/oauth1_common.js @@ -0,0 +1 @@ +Meteor.accounts.oauth1 = {}; \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js new file mode 100644 index 0000000000..cced70c2ce --- /dev/null +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -0,0 +1,150 @@ +var crypto = __meteor_bootstrap__.require("crypto"); +var querystring = __meteor_bootstrap__.require("querystring"); + +(function () { + var connect = __meteor_bootstrap__.require("connect"); + + Meteor.accounts.oauth1._services = {}; + + // Register a handler for an OAuth1 service. The handler will be called + // when we get an incoming http request on /_oauth1/{serviceName}. This + // handler should use that information to fetch data about the user + // logging in. + // + // @param name {String} e.g. "flickr", "twitter" + // @param handleOauthRequest {Function(query)} + // - query is an object with the parameters passed in the query string + // - return value is: + // - {options: (options), extra: (optional extra)} (same as the + // arguments to Meteor.accounts.updateOrCreateUser) + // - `null` if the user declined to give permissions + // XXX In the context of oauth1 the name handleOauthRequest doesn't make as much sense + Meteor.accounts.oauth1.registerService = function (name, handleOauthRequest) { + if (Meteor.accounts.oauth1._services[name]) + throw new Error("Already registered the " + name + " OAuth1 service"); + + Meteor.accounts.oauth1._services[name] = { + handleOauthRequest: handleOauthRequest + }; + }; + + // Listen to calls to `login` with an oauth option set + Meteor.accounts.registerLoginHandler(function (options) { + if (!options.oauth || options.oauth.version !== 1) + return undefined; // don't handle + + var result = Meteor.accounts.oauth1._loginResultForState[options.oauth.state]; + if (result === undefined) // not using `!result` since can be null + // We weren't notified of the user authorizing the login. + return null; + else + return result; + }); + + // When we get an incoming OAuth http request we complete the oauth + // handshake, account and token setup before responding. The + // results are stored in this map which is then read when the login + // method is called. Maps state --> return value of `login` + // + // XXX we should periodically clear old entries + Meteor.accounts.oauth1._loginResultForState = {}; + + // connect middleware + Meteor.accounts.oauth1._handleRequest = function (req, res, next) { + + // req.url will be "/_oauth1/?" + var barePath = req.url.substring(0, req.url.indexOf('?')); + var splitPath = barePath.split('/'); + + // Any non-oauth request will continue down the default middlewares + if (splitPath[1] !== '_oauth1') { + next(); + return; + } + + // Make sure we prepare the login results before returning. + // This way the subsequent call to the `login` method will be + // immediate. + + var serviceName = splitPath[2]; + + // XXX check against a list of installed services too + if (!serviceName) + throw new Meteor.accounts.ConfigError("Service could not be found"); + + // Make sure we're configured + if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); + if (!Meteor.accounts[serviceName]._secret) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + + var service = Meteor.accounts.oauth1._services[serviceName]; + var config = Meteor.accounts[serviceName]; + var oauth = new OAuth(config); + + if (req.query.callbackUrl) { + + // Get a request token to start auth process + oauth.getRequestToken(req.query.callbackUrl); + + var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauth.requestToken; + res.writeHead(302, {'Location': redirectUrl}); + res.end(); + + } else { + + // XXX does checking for the verifier really make sense? + if (!req.query.oauth_token || !req.query.oauth_verifier) { + // The user didn't authorize access + return null; + } + + // Get the oauth token for signing requests + oauth.getAccessToken(req.query.oauth_token); + + // Get or create user id + var oauthResult = service.handleOauthRequest(oauth); + + if (oauthResult) { // could be null if user declined permissions + var userId = Meteor.accounts.updateOrCreateUser(oauthResult.options, oauthResult.extra); + + // Generate and store a login token for reconnect + // XXX this could go in accounts_server.js instead + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + + // Store results to subsequent call to `login` + Meteor.accounts.oauth1._loginResultForState[req.query.state] = + {token: loginToken, id: userId}; + } + + // We support ?close and ?redirect=URL. Any other query should + // just serve a blank page + if ('close' in req.query) { // check with 'in' because we don't set a value + // Close the popup window + res.writeHead(200, {'Content-Type': 'text/html'}); + var content = + ''; + res.end(content, 'utf-8'); + } else if (req.query.redirect) { + res.writeHead(302, {'Location': req.query.redirect}); + res.end(); + } else { + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end('', 'utf-8'); + } + } + }; + + // Listen on /_oauth/* + __meteor_bootstrap__.app + .use(connect.query()) + .use(function(req, res, next) { + // Need to create a Fiber since we're using synchronous http + // calls and nothing else is wrapping this in a fiber + // automatically + Fiber(function () { + Meteor.accounts.oauth1._handleRequest(req, res, next); + }).run(); + }); + +})(); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js new file mode 100644 index 0000000000..826793bb60 --- /dev/null +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -0,0 +1,39 @@ +Tinytest.add("oauth2 - loginResultForState is stored", function (test) { + var http = __meteor_bootstrap__.require('http'); + var email = Meteor.uuid() + "@example.com"; + + Meteor.accounts._loginTokens.remove({}); + Meteor.accounts.oauth1._loginResultForState = {}; + Meteor.accounts.oauth1._services = {}; + + // register a fake login service - foobook + Meteor.accounts.oauth1.registerService("foobook", function (query) { + return { + options: { + email: email, + services: {foobook: {id: 1}} + } + }; + }); + + // simulate logging in using foobook + var req = {method: "POST", + url: "/_oauth1/foobook?close", + query: {state: "STATE"}}; + Meteor.accounts.oauth1._handleRequest(req, new http.ServerResponse(req)); + + // verify that a user is created + var user = Meteor.users.findOne({emails: email}); + test.notEqual(user, undefined); + test.equal(user.services.foobook.id, 1); + + // and that that user has a login token + var token = Meteor.accounts._loginTokens.findOne({userId: user._id}); + test.notEqual(token, undefined); + + // and that the login result for that user is prepared + test.equal( + Meteor.accounts.oauth1._loginResultForState['STATE'].id, user._id); + test.equal( + Meteor.accounts.oauth1._loginResultForState['STATE'].token, token._id); +}); diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js new file mode 100644 index 0000000000..57d67e85d3 --- /dev/null +++ b/packages/accounts-oauth1-helper/package.js @@ -0,0 +1,19 @@ +Package.describe({ + summary: "Common code for OAuth1-based login services", + internal: true +}); + +Package.on_use(function (api) { + api.use('accounts', ['client', 'server']); + api.use('oauth1', 'server'); + + api.add_files('oauth1_common.js', ['client', 'server']); + api.add_files('oauth1_server.js', 'server'); + api.add_files('oauth1_client.js', 'client'); +}); + +Package.on_test(function (api) { + // XXX Fix these! + // api.use('accounts-oauth1-helper', 'server'); + // api.add_files("oauth1_tests.js", 'server'); +}); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 03e8819f92..1409589579 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -26,7 +26,7 @@ // Listen to calls to `login` with an oauth option set Meteor.accounts.registerLoginHandler(function (options) { - if (!options.oauth) + if (!options.oauth || options.oauth.version !== 2) return undefined; // don't handle var result = Meteor.accounts.oauth2._loginResultForState[options.oauth.state]; diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js new file mode 100644 index 0000000000..a8e34036e9 --- /dev/null +++ b/packages/accounts-twitter/package.js @@ -0,0 +1,13 @@ +Package.describe({ + summary: "Login service for Twitter accounts" +}); + +Package.on_use(function(api) { + api.use('accounts', ['client', 'server']); + api.use('accounts-oauth1-helper', ['client', 'server']); + api.use('http', ['client', 'server']); + + api.add_files('twitter_common.js', ['client', 'server']); + api.add_files('twitter_server.js', 'server'); + api.add_files('twitter_client.js', 'client'); +}); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js new file mode 100644 index 0000000000..55f44f0973 --- /dev/null +++ b/packages/accounts-twitter/twitter_client.js @@ -0,0 +1,13 @@ +(function () { + Meteor.loginWithTwitter = function () { + if (!Meteor.accounts.twitter._appId || !Meteor.accounts.twitter._appUrl) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first"); + + var state = Meteor.uuid(); + var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth1/twitter?close&state=' + state; + var url = '/_oauth1/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) + + Meteor.accounts.oauth1.initiateLogin(state, url); + }; + +})(); diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js new file mode 100644 index 0000000000..62f4d8d373 --- /dev/null +++ b/packages/accounts-twitter/twitter_common.js @@ -0,0 +1,16 @@ +if (!Meteor.accounts.twitter) { + Meteor.accounts.twitter = {}; +} + +Meteor.accounts.twitter.config = function(appId, appUrl, options) { + Meteor.accounts.twitter._appId = appId; + Meteor.accounts.twitter._appUrl = appUrl; + Meteor.accounts.twitter._options = options; + + Meteor.accounts.twitter._urls = { + requestToken: "https://api.twitter.com/oauth/request_token", + authorize: "https://api.twitter.com/oauth/authorize", + accessToken: "https://api.twitter.com/oauth/access_token", + authenticate: "https://api.twitter.com/oauth/authenticate" + }; +}; diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js new file mode 100644 index 0000000000..6dcae80017 --- /dev/null +++ b/packages/accounts-twitter/twitter_server.js @@ -0,0 +1,20 @@ +(function () { + + Meteor.accounts.twitter.setSecret = function (secret) { + Meteor.accounts.twitter._secret = secret; + }; + + Meteor.accounts.oauth1.registerService('twitter', function(oauth) { + + var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); + + return { + options: { + // XXX Figure out what to do here + email: identity.screen_name + '@OAUTH1_TWITTER', + services: {twitter: {id: identity.id, accessToken: oauth.accessToken}} + }, + extra: {name: identity.name} + }; + }); +}) (); diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index f060816094..506960a27a 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -69,6 +69,19 @@ }; }, + 'click #login-buttons-Twitter': function () { + try { + Meteor.loginWithTwitter(); + } catch (e) { + if (e instanceof Meteor.accounts.ConfigError) + alert("Twitter API key not set. Configure app details with " + + "Meteor.accounts.twitter.config() and " + + "Meteor.accounts.twitter.setSecret()"); + else + throw e; + }; + }, + 'click #login-buttons-logout': function() { Meteor.logout(); resetSession(); @@ -540,6 +553,8 @@ ret.push({name: 'Google'}); if (Meteor.accounts.weibo) ret.push({name: 'Weibo'}); + if (Meteor.accounts.twitter) + ret.push({name: 'Twitter'}); // make sure to put accounts last, since this is the order in the // ui as well diff --git a/packages/accounts-ui/login_buttons_images.css b/packages/accounts-ui/login_buttons_images.css index 8d11767e12..88f9b54546 100644 --- a/packages/accounts-ui/login_buttons_images.css +++ b/packages/accounts-ui/login_buttons_images.css @@ -1,3 +1,5 @@ +/* These should be in their respective packages */ + #login-buttons-image-Google { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==); } @@ -9,3 +11,7 @@ #login-buttons-image-Weibo { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=); } + +#login-buttons-image-Twitter { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=); +} diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js new file mode 100644 index 0000000000..1e7580c224 --- /dev/null +++ b/packages/oauth1/oauth1.js @@ -0,0 +1,122 @@ + +// XXX Use oauth verifier + +OAuth = function(config) { + this.config = config; +}; + +OAuth.prototype._getAuthHeaderString = function(headers) { + return 'OAuth ' + _.map(headers, function(val, key) { + return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; + }).sort().join(', '); +}; + +OAuth.prototype.getRequestToken = function(callbackUrl) { + + var headers = this._buildHeader({ + oauth_callback: callbackUrl + }); + + headers.oauth_signature = this._getSignature('POST', this.config._urls.requestToken, headers); + + var authString = this._getAuthHeaderString(headers); + + var response = Meteor.http.post(this.config._urls.requestToken, { + headers: { + Authorization: authString + } + }); + + if (response.error) + throw response.error; + + var tokens = querystring.parse(response.content); + this.requestToken = tokens.oauth_token; +}; + +OAuth.prototype.getAccessToken = function(oauthToken) { + + var headers = this._buildHeader({ + oauth_token: oauthToken + }); + + headers.oauth_signature = this._getSignature('POST', this.config._urls.accessToken, headers); + + var authString = this._getAuthHeaderString(headers); + + var response = Meteor.http.post(this.config._urls.accessToken, { + headers: { + Authorization: authString + } + }); + + if (response.error) + throw response.error; + + var tokens = querystring.parse(response.content); + this.accessToken = tokens.oauth_token; + this.accessTokenSecret = tokens.oauth_token_secret; +}; + +OAuth.prototype.call = function(method, url) { + var headers = this._buildHeader({ + oauth_token: this.accessToken + }); + + headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); + + var authString = this._getAuthHeaderString(headers); + + var response = Meteor.http[method.toLowerCase()](url, { + headers: { + Authorization: authString + } + }); + + if (response.error) + throw response.error; + + return response.data; +}; + +OAuth.prototype.get = function(url) { + return this.call('get', url); +}; + +OAuth.prototype._buildHeader = function(headers) { + return _.extend({ + oauth_consumer_key: this.config._appId, + oauth_nonce: Meteor.uuid().replace(/\W/g, ''), + oauth_signature_method: 'HMAC-SHA1', + oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), + oauth_version: '1.0' + }, headers); +}; + +OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { + + var headers = this._encodeHeader(rawHeaders); + + var parameters = _.map(headers, function(val, key) { + return key + '=' + val; + }).sort().join('&'); + + var signatureBase = [ + method, + encodeURIComponent(url), + encodeURIComponent(parameters) + ].join('&'); + + var signingKey = encodeURIComponent(this.config._secret) + '&'; + if (oauthSecret) + signingKey += encodeURIComponent(oauthSecret); + + return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); +}; + +OAuth.prototype._encodeHeader = function(header) { + return _.reduce(header, function(memo, val, key) { + memo[encodeURIComponent(key)] = encodeURIComponent(val); + return memo; + }, {}); +}; diff --git a/packages/oauth1/package.js b/packages/oauth1/package.js new file mode 100644 index 0000000000..a7afe9424e --- /dev/null +++ b/packages/oauth1/package.js @@ -0,0 +1,11 @@ +Package.describe({ + summary: "Code for oauth1 clients", +}); + +Package.on_use(function (api) { + api.add_files('oauth1.js', 'server'); +}); + +Package.on_test(function (api) { + // XXX Add some! +}); From ca609e5918ccefbed5eeea7c431e3ffded4f9d87 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 20:39:35 -0400 Subject: [PATCH 02/38] added some comments --- packages/accounts-oauth1-helper/oauth1_server.js | 5 +++++ packages/accounts-twitter/twitter_server.js | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index cced70c2ce..171d94a7d3 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -82,6 +82,8 @@ var querystring = __meteor_bootstrap__.require("querystring"); var config = Meteor.accounts[serviceName]; var oauth = new OAuth(config); + // If we get here with a callback url we need a request token to + // start the logic process if (req.query.callbackUrl) { // Get a request token to start auth process @@ -91,6 +93,9 @@ var querystring = __meteor_bootstrap__.require("querystring"); res.writeHead(302, {'Location': redirectUrl}); res.end(); + // If we get here without a callback url we've just + // returned from authentication via the oauth provider + } else { // XXX does checking for the verifier really make sense? diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 6dcae80017..63b626747d 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -12,6 +12,7 @@ options: { // XXX Figure out what to do here email: identity.screen_name + '@OAUTH1_TWITTER', + // XXX Do we want to keep the accessTokenSecret also? services: {twitter: {id: identity.id, accessToken: oauth.accessToken}} }, extra: {name: identity.name} From 107051a3f379093645b574b2751d9a3b02bbfc4c Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 20:45:42 -0400 Subject: [PATCH 03/38] extend oauth with configuration --- packages/oauth1/oauth1.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index 1e7580c224..60dcfec042 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -2,7 +2,7 @@ // XXX Use oauth verifier OAuth = function(config) { - this.config = config; + _.extend(this, config); }; OAuth.prototype._getAuthHeaderString = function(headers) { @@ -17,11 +17,11 @@ OAuth.prototype.getRequestToken = function(callbackUrl) { oauth_callback: callbackUrl }); - headers.oauth_signature = this._getSignature('POST', this.config._urls.requestToken, headers); + headers.oauth_signature = this._getSignature('POST', this._urls.requestToken, headers); var authString = this._getAuthHeaderString(headers); - var response = Meteor.http.post(this.config._urls.requestToken, { + var response = Meteor.http.post(this._urls.requestToken, { headers: { Authorization: authString } @@ -40,11 +40,11 @@ OAuth.prototype.getAccessToken = function(oauthToken) { oauth_token: oauthToken }); - headers.oauth_signature = this._getSignature('POST', this.config._urls.accessToken, headers); + headers.oauth_signature = this._getSignature('POST', this._urls.accessToken, headers); var authString = this._getAuthHeaderString(headers); - var response = Meteor.http.post(this.config._urls.accessToken, { + var response = Meteor.http.post(this._urls.accessToken, { headers: { Authorization: authString } @@ -85,7 +85,7 @@ OAuth.prototype.get = function(url) { OAuth.prototype._buildHeader = function(headers) { return _.extend({ - oauth_consumer_key: this.config._appId, + oauth_consumer_key: this._appId, oauth_nonce: Meteor.uuid().replace(/\W/g, ''), oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), @@ -107,7 +107,7 @@ OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { encodeURIComponent(parameters) ].join('&'); - var signingKey = encodeURIComponent(this.config._secret) + '&'; + var signingKey = encodeURIComponent(this._secret) + '&'; if (oauthSecret) signingKey += encodeURIComponent(oauthSecret); From 08a53cabcad1b9683dfc91bf58b6c3d120afe079 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 20:53:57 -0400 Subject: [PATCH 04/38] cleanup oauth1 class --- packages/oauth1/oauth1.js | 50 +++++++++++++++------------------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index 60dcfec042..28e9bed8fa 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -17,20 +17,9 @@ OAuth.prototype.getRequestToken = function(callbackUrl) { oauth_callback: callbackUrl }); - headers.oauth_signature = this._getSignature('POST', this._urls.requestToken, headers); - - var authString = this._getAuthHeaderString(headers); - - var response = Meteor.http.post(this._urls.requestToken, { - headers: { - Authorization: authString - } - }); - - if (response.error) - throw response.error; - + var response = this._call('post', this._urls.requestToken, headers); var tokens = querystring.parse(response.content); + this.requestToken = tokens.oauth_token; }; @@ -40,33 +29,22 @@ OAuth.prototype.getAccessToken = function(oauthToken) { oauth_token: oauthToken }); - headers.oauth_signature = this._getSignature('POST', this._urls.accessToken, headers); - - var authString = this._getAuthHeaderString(headers); - - var response = Meteor.http.post(this._urls.accessToken, { - headers: { - Authorization: authString - } - }); - - if (response.error) - throw response.error; - + var response = this._call('post', this._urls.accessToken, headers); var tokens = querystring.parse(response.content); + this.accessToken = tokens.oauth_token; this.accessTokenSecret = tokens.oauth_token_secret; }; -OAuth.prototype.call = function(method, url) { - var headers = this._buildHeader({ - oauth_token: this.accessToken - }); - +OAuth.prototype._call = function(method, url, headers) { + + // Get the signature headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); + // Make a authorization string according to oauth1 spec var authString = this._getAuthHeaderString(headers); + // Make signed request var response = Meteor.http[method.toLowerCase()](url, { headers: { Authorization: authString @@ -75,6 +53,16 @@ OAuth.prototype.call = function(method, url) { if (response.error) throw response.error; + + return response; +}; + +OAuth.prototype.call = function(method, url) { + var headers = this._buildHeader({ + oauth_token: this.accessToken + }); + + var response = this._call(method, url, headers); return response.data; }; From 7dfb385fc3f228b381daa3ec7fe4bf0653e7c6fd Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 20:57:47 -0400 Subject: [PATCH 05/38] move methods around --- packages/oauth1/oauth1.js | 54 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index 28e9bed8fa..d24e7f817d 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -5,12 +5,6 @@ OAuth = function(config) { _.extend(this, config); }; -OAuth.prototype._getAuthHeaderString = function(headers) { - return 'OAuth ' + _.map(headers, function(val, key) { - return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; - }).sort().join(', '); -}; - OAuth.prototype.getRequestToken = function(callbackUrl) { var headers = this._buildHeader({ @@ -36,27 +30,6 @@ OAuth.prototype.getAccessToken = function(oauthToken) { this.accessTokenSecret = tokens.oauth_token_secret; }; -OAuth.prototype._call = function(method, url, headers) { - - // Get the signature - headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); - - // Make a authorization string according to oauth1 spec - var authString = this._getAuthHeaderString(headers); - - // Make signed request - var response = Meteor.http[method.toLowerCase()](url, { - headers: { - Authorization: authString - } - }); - - if (response.error) - throw response.error; - - return response; -}; - OAuth.prototype.call = function(method, url) { var headers = this._buildHeader({ oauth_token: this.accessToken @@ -102,9 +75,36 @@ OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); }; +OAuth.prototype._call = function(method, url, headers) { + + // Get the signature + headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); + + // Make a authorization string according to oauth1 spec + var authString = this._getAuthHeaderString(headers); + + // Make signed request + var response = Meteor.http[method.toLowerCase()](url, { + headers: { + Authorization: authString + } + }); + + if (response.error) + throw response.error; + + return response; +}; + OAuth.prototype._encodeHeader = function(header) { return _.reduce(header, function(memo, val, key) { memo[encodeURIComponent(key)] = encodeURIComponent(val); return memo; }, {}); }; + +OAuth.prototype._getAuthHeaderString = function(headers) { + return 'OAuth ' + _.map(headers, function(val, key) { + return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; + }).sort().join(', '); +}; From 2915d907e10dda39937497de5b6231ef939ff1fb Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 21:06:41 -0400 Subject: [PATCH 06/38] added comments --- packages/accounts-oauth1-helper/oauth1_server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 171d94a7d3..389418c9b3 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -52,6 +52,10 @@ var querystring = __meteor_bootstrap__.require("querystring"); // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { + // XXX Used _oauth1 so routing can differentiate between oauth1 and 2 + // XXX I think the solution is to use a regex that includes the + // applicable providers, e.g. oauth 1 regex would contain (twitter|flickr) + // req.url will be "/_oauth1/?" var barePath = req.url.substring(0, req.url.indexOf('?')); var splitPath = barePath.split('/'); From 1bffb63b63a021f2d2b990b1db1295893b07bc09 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 21:20:00 -0400 Subject: [PATCH 07/38] move requires to correct file --- packages/accounts-oauth1-helper/oauth1_server.js | 3 --- packages/oauth1/oauth1.js | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 389418c9b3..9506968cf9 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -1,6 +1,3 @@ -var crypto = __meteor_bootstrap__.require("crypto"); -var querystring = __meteor_bootstrap__.require("querystring"); - (function () { var connect = __meteor_bootstrap__.require("connect"); diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index d24e7f817d..0279c63622 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -1,3 +1,5 @@ +var crypto = __meteor_bootstrap__.require("crypto"); +var querystring = __meteor_bootstrap__.require("querystring"); // XXX Use oauth verifier From 42ca4de679b3c8f7736c9af12736ff5c7d01eaa5 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 21:32:33 -0400 Subject: [PATCH 08/38] use oauth verifier --- .../accounts-oauth1-helper/oauth1_server.js | 4 +++- packages/oauth1/oauth1.js | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 9506968cf9..e52e4f65a2 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -99,6 +99,8 @@ } else { + // XXX Twitter's docs say to check that oauth_token is the + // same as the request token received in previous step // XXX does checking for the verifier really make sense? if (!req.query.oauth_token || !req.query.oauth_verifier) { // The user didn't authorize access @@ -106,7 +108,7 @@ } // Get the oauth token for signing requests - oauth.getAccessToken(req.query.oauth_token); + oauth.getAccessToken(req.query); // Get or create user id var oauthResult = service.handleOauthRequest(oauth); diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index 0279c63622..929504a67a 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -1,8 +1,6 @@ var crypto = __meteor_bootstrap__.require("crypto"); var querystring = __meteor_bootstrap__.require("querystring"); -// XXX Use oauth verifier - OAuth = function(config) { _.extend(this, config); }; @@ -19,13 +17,16 @@ OAuth.prototype.getRequestToken = function(callbackUrl) { this.requestToken = tokens.oauth_token; }; -OAuth.prototype.getAccessToken = function(oauthToken) { - +OAuth.prototype.getAccessToken = function(query) { var headers = this._buildHeader({ - oauth_token: oauthToken + oauth_token: query.oauth_token }); - var response = this._call('post', this._urls.accessToken, headers); + var params = { + oauth_verifier: query.oauth_verifier + }; + + var response = this._call('post', this._urls.accessToken, headers, params); var tokens = querystring.parse(response.content); this.accessToken = tokens.oauth_token; @@ -36,7 +37,7 @@ OAuth.prototype.call = function(method, url) { var headers = this._buildHeader({ oauth_token: this.accessToken }); - + var response = this._call(method, url, headers); return response.data; @@ -77,7 +78,7 @@ OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); }; -OAuth.prototype._call = function(method, url, headers) { +OAuth.prototype._call = function(method, url, headers, params) { // Get the signature headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); @@ -87,6 +88,7 @@ OAuth.prototype._call = function(method, url, headers) { // Make signed request var response = Meteor.http[method.toLowerCase()](url, { + params: params, headers: { Authorization: authString } From 5f9485d1330ca21464a124718d652281acf94dc4 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Sun, 29 Jul 2012 22:26:38 -0400 Subject: [PATCH 09/38] better comment, removed unnecessary check --- packages/accounts-oauth1-helper/oauth1_server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index e52e4f65a2..a71fe55f97 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -101,8 +101,8 @@ // XXX Twitter's docs say to check that oauth_token is the // same as the request token received in previous step - // XXX does checking for the verifier really make sense? - if (!req.query.oauth_token || !req.query.oauth_verifier) { + + if (!req.query.oauth_token) { // The user didn't authorize access return null; } From 9c8fd2958e1f1bade4d7c8c7d5c2fe9ec1978135 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Wed, 1 Aug 2012 06:09:41 -0400 Subject: [PATCH 10/38] nicer way of handling /_oauth/ routes --- .../accounts-oauth1-helper/oauth1_server.js | 22 ++++----- .../accounts-oauth1-helper/oauth1_tests.js | 2 +- .../accounts-oauth2-helper/oauth2_server.js | 48 +++++++------------ packages/accounts-twitter/twitter_client.js | 4 +- 4 files changed, 27 insertions(+), 49 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index a71fe55f97..f58991f478 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -4,7 +4,7 @@ Meteor.accounts.oauth1._services = {}; // Register a handler for an OAuth1 service. The handler will be called - // when we get an incoming http request on /_oauth1/{serviceName}. This + // when we get an incoming http request on /_oauth/{serviceName}. This // handler should use that information to fetch data about the user // logging in. // @@ -49,16 +49,17 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { - // XXX Used _oauth1 so routing can differentiate between oauth1 and 2 - // XXX I think the solution is to use a regex that includes the - // applicable providers, e.g. oauth 1 regex would contain (twitter|flickr) - - // req.url will be "/_oauth1/?" + // req.url will be "/_oauth/?" var barePath = req.url.substring(0, req.url.indexOf('?')); var splitPath = barePath.split('/'); + // Find service based on url + var serviceName = splitPath[2]; + var service = Meteor.accounts.oauth1._services[serviceName]; + // Any non-oauth request will continue down the default middlewares - if (splitPath[1] !== '_oauth1') { + // Same goes for service that hasn't been registered + if (splitPath[1] !== '_oauth' || !service) { next(); return; } @@ -67,19 +68,12 @@ // This way the subsequent call to the `login` method will be // immediate. - var serviceName = splitPath[2]; - - // XXX check against a list of installed services too - if (!serviceName) - throw new Meteor.accounts.ConfigError("Service could not be found"); - // Make sure we're configured if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); if (!Meteor.accounts[serviceName]._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); - var service = Meteor.accounts.oauth1._services[serviceName]; var config = Meteor.accounts[serviceName]; var oauth = new OAuth(config); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 826793bb60..afa341dcb4 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -18,7 +18,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // simulate logging in using foobook var req = {method: "POST", - url: "/_oauth1/foobook?close", + url: "/_oauth/foobook?close", query: {state: "STATE"}}; Meteor.accounts.oauth1._handleRequest(req, new http.ServerResponse(req)); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 1409589579..6853951020 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -56,8 +56,13 @@ var barePath = req.url.substring(0, req.url.indexOf('?')); var splitPath = barePath.split('/'); + // Find service based on url + var serviceName = splitPath[2]; + var service = Meteor.accounts.oauth2._services[serviceName]; + // Any non-oauth request will continue down the default middlewares - if (splitPath[1] !== '_oauth') { + // Same goes for service that hasn't been registered + if (splitPath[1] !== '_oauth' || !service) { next(); return; } @@ -66,40 +71,19 @@ // This way the subsequent call to the `login` method will be // immediate. - var serviceName = splitPath[2]; - var service = Meteor.accounts.oauth2._services[serviceName]; + // Get or create user id + var oauthResult = service.handleOauthRequest(req.query); - try { - // Get or create user id - var oauthResult = service && service.handleOauthRequest(req.query); + if (oauthResult) { // could be null if user declined permissions + var userId = Meteor.accounts.updateOrCreateUser(oauthResult.options, oauthResult.extra); - // could be null if user declined permissions, or if there was an - // error of some sort. - if (oauthResult && req.query.state) { - var userId = Meteor.accounts.updateOrCreateUser( - oauthResult.options, oauthResult.extra); + // Generate and store a login token for reconnect + // XXX this could go in accounts_server.js instead + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - // Generate and store a login token for reconnect - // XXX this could go in accounts_server.js instead - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - - // Store results to subsequent call to `login` - Meteor.accounts.oauth2._loginResultForState[req.query.state] = - {token: loginToken, id: userId}; - } - } catch (err) { - // if we got thrown an error, save it off, it will get passed to - // the approporiate login call (if any) and reported there. - // - // The other option would be to display it in the popup tab that - // is still open at this point, ignoring the 'close' or 'redirect' - // we were passed. But then the developer wouldn't be able to - // style the error or react to it in any way. - if (req.query.state && err instanceof Error) - Meteor.accounts.oauth2._loginResultForState[req.query.state] = err; - - // also log to the server console, so the developer sees it. - Meteor._debug("Exception in oauth2 handler", err); + // Store results to subsequent call to `login` + Meteor.accounts.oauth2._loginResultForState[req.query.state] = + {token: loginToken, id: userId}; } // We support ?close and ?redirect=URL. Any other query should diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 55f44f0973..40d36b1482 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -4,8 +4,8 @@ throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first"); var state = Meteor.uuid(); - var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth1/twitter?close&state=' + state; - var url = '/_oauth1/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) + var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; + var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) Meteor.accounts.oauth1.initiateLogin(state, url); }; From 7d7ac074df8248264e4c66d3aee3568a319aae6f Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 00:31:57 -0400 Subject: [PATCH 11/38] better munging twitter data for updateOrCreateUser --- packages/accounts-twitter/twitter_server.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 63b626747d..23d35575b2 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -10,12 +10,17 @@ return { options: { - // XXX Figure out what to do here - email: identity.screen_name + '@OAUTH1_TWITTER', - // XXX Do we want to keep the accessTokenSecret also? - services: {twitter: {id: identity.id, accessToken: oauth.accessToken}} + services: { + twitter: { + id: identity.id, + screenName: identity.screen_name, + accessToken: oauth.accessToken + } + } }, - extra: {name: identity.name} + extra: { + name: identity.name + } }; }); }) (); From 98eaf61bef760cc1ee0c7d59ab26fc79d18a9590 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 00:35:56 -0400 Subject: [PATCH 12/38] renamed OAuth to OAuth1 --- .../accounts-oauth1-helper/oauth1_server.js | 2 +- packages/oauth1/oauth1.js | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index f58991f478..5fb0cf1021 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -75,7 +75,7 @@ throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); var config = Meteor.accounts[serviceName]; - var oauth = new OAuth(config); + var oauth = new OAuth1(config); // If we get here with a callback url we need a request token to // start the logic process diff --git a/packages/oauth1/oauth1.js b/packages/oauth1/oauth1.js index 929504a67a..324eef621e 100644 --- a/packages/oauth1/oauth1.js +++ b/packages/oauth1/oauth1.js @@ -1,11 +1,11 @@ var crypto = __meteor_bootstrap__.require("crypto"); var querystring = __meteor_bootstrap__.require("querystring"); -OAuth = function(config) { +OAuth1 = function(config) { _.extend(this, config); }; -OAuth.prototype.getRequestToken = function(callbackUrl) { +OAuth1.prototype.getRequestToken = function(callbackUrl) { var headers = this._buildHeader({ oauth_callback: callbackUrl @@ -17,7 +17,7 @@ OAuth.prototype.getRequestToken = function(callbackUrl) { this.requestToken = tokens.oauth_token; }; -OAuth.prototype.getAccessToken = function(query) { +OAuth1.prototype.getAccessToken = function(query) { var headers = this._buildHeader({ oauth_token: query.oauth_token }); @@ -33,7 +33,7 @@ OAuth.prototype.getAccessToken = function(query) { this.accessTokenSecret = tokens.oauth_token_secret; }; -OAuth.prototype.call = function(method, url) { +OAuth1.prototype.call = function(method, url) { var headers = this._buildHeader({ oauth_token: this.accessToken }); @@ -43,11 +43,11 @@ OAuth.prototype.call = function(method, url) { return response.data; }; -OAuth.prototype.get = function(url) { +OAuth1.prototype.get = function(url) { return this.call('get', url); }; -OAuth.prototype._buildHeader = function(headers) { +OAuth1.prototype._buildHeader = function(headers) { return _.extend({ oauth_consumer_key: this._appId, oauth_nonce: Meteor.uuid().replace(/\W/g, ''), @@ -57,7 +57,7 @@ OAuth.prototype._buildHeader = function(headers) { }, headers); }; -OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { +OAuth1.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { var headers = this._encodeHeader(rawHeaders); @@ -78,7 +78,7 @@ OAuth.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); }; -OAuth.prototype._call = function(method, url, headers, params) { +OAuth1.prototype._call = function(method, url, headers, params) { // Get the signature headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); @@ -100,14 +100,14 @@ OAuth.prototype._call = function(method, url, headers, params) { return response; }; -OAuth.prototype._encodeHeader = function(header) { +OAuth1.prototype._encodeHeader = function(header) { return _.reduce(header, function(memo, val, key) { memo[encodeURIComponent(key)] = encodeURIComponent(val); return memo; }, {}); }; -OAuth.prototype._getAuthHeaderString = function(headers) { +OAuth1.prototype._getAuthHeaderString = function(headers) { return 'OAuth ' + _.map(headers, function(val, key) { return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; }).sort().join(', '); From 63eeec47081cf7eee44e477e93cefcecbf517900 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 01:02:07 -0400 Subject: [PATCH 13/38] moved common oauth 1 and 2 client code to accounts-oauth-helper package --- .../accounts-oauth-helper/oauth_client.js | 62 +++++++++++++++++++ .../accounts-oauth-helper/oauth_common.js | 1 + packages/accounts-oauth-helper/package.js | 17 +++++ .../accounts-oauth1-helper/oauth1_client.js | 56 +---------------- packages/accounts-oauth1-helper/package.js | 2 +- .../accounts-oauth2-helper/oauth2_client.js | 58 +---------------- packages/accounts-oauth2-helper/package.js | 1 + 7 files changed, 86 insertions(+), 111 deletions(-) create mode 100644 packages/accounts-oauth-helper/oauth_client.js create mode 100644 packages/accounts-oauth-helper/oauth_common.js create mode 100644 packages/accounts-oauth-helper/package.js diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js new file mode 100644 index 0000000000..7640f1a36d --- /dev/null +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -0,0 +1,62 @@ +(function () { + // Open a popup window pointing to a OAuth handshake page + // + // @param state {String} The OAuth state generated by the client + // @param url {String} url to page + Meteor.accounts.oauth.initiateLogin = function(state, url, version) { + // XXX these dimensions worked well for facebook and google, but + // it's sort of weird to have these here. Maybe an optional + // argument instead? + var popup = openCenteredPopup(url, 650, 331); + + var checkPopupOpen = setInterval(function() { + if (popup.closed) { + clearInterval(checkPopupOpen); + tryLoginAfterPopupClosed(state, version); + } + }, 100); + }; + + // Send an OAuth login method to the server. If the user authorized + // access in the popup this should log the user in, otherwise + // nothing should happen. + var tryLoginAfterPopupClosed = function(state, version) { + Meteor.apply('login', [ + {oauth: {version: version, state: state}} + ], {wait: true}, function(error, result) { + if (error) + throw error; + + if (!result) { + // The user either closed the OAuth popup or didn't authorize + // access. Do nothing. + return; + } else { + Meteor.accounts.makeClientLoggedIn(result.id, result.token); + } + }); + }; + + var openCenteredPopup = function(url, width, height) { + var screenX = typeof window.screenX !== 'undefined' + ? window.screenX : window.screenLeft; + var screenY = typeof window.screenY !== 'undefined' + ? window.screenY : window.screenTop; + var outerWidth = typeof window.outerWidth !== 'undefined' + ? window.outerWidth : document.body.clientWidth; + var outerHeight = typeof window.outerHeight !== 'undefined' + ? window.outerHeight : (document.body.clientHeight - 22); + + // Use `outerWidth - width` and `outerHeight - height` for help in + // positioning the popup centered relative to the current window + var left = screenX + (outerWidth - width) / 2; + var top = screenY + (outerHeight - height) / 2; + var features = ('width=' + width + ',height=' + height + + ',left=' + left + ',top=' + top); + + var newwindow = window.open(url, 'Login', features); + if (newwindow.focus) + newwindow.focus(); + return newwindow; + }; +})(); \ No newline at end of file diff --git a/packages/accounts-oauth-helper/oauth_common.js b/packages/accounts-oauth-helper/oauth_common.js new file mode 100644 index 0000000000..3c6e8b8558 --- /dev/null +++ b/packages/accounts-oauth-helper/oauth_common.js @@ -0,0 +1 @@ +Meteor.accounts.oauth = {}; \ No newline at end of file diff --git a/packages/accounts-oauth-helper/package.js b/packages/accounts-oauth-helper/package.js new file mode 100644 index 0000000000..cacb1c06b9 --- /dev/null +++ b/packages/accounts-oauth-helper/package.js @@ -0,0 +1,17 @@ +Package.describe({ + summary: "Common code for OAuth-based login services", + internal: true +}); + +Package.on_use(function (api) { + api.use('accounts', ['client', 'server']); + + api.add_files('oauth_common.js', ['client', 'server']); + api.add_files('oauth_client.js', 'client'); +}); + +Package.on_test(function (api) { + // XXX Fix these! + // api.use('accounts-oauth-helper', 'server'); + // api.add_files("oauth_tests.js", 'server'); +}); diff --git a/packages/accounts-oauth1-helper/oauth1_client.js b/packages/accounts-oauth1-helper/oauth1_client.js index f859ff69cf..f72deb3779 100644 --- a/packages/accounts-oauth1-helper/oauth1_client.js +++ b/packages/accounts-oauth1-helper/oauth1_client.js @@ -4,59 +4,7 @@ // @param state {String} The OAuth1 state generated by the client // @param url {String} url to page Meteor.accounts.oauth1.initiateLogin = function(state, url) { - // XXX these dimensions worked well for facebook and google, but - // it's sort of weird to have these here. Maybe an optional - // argument instead? - var popup = openCenteredPopup(url, 650, 331); - - var checkPopupOpen = setInterval(function() { - if (popup.closed) { - clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state); - } - }, 100); - }; - - // Send an OAuth login method to the server. If the user authorized - // access in the popup this should log the user in, otherwise - // nothing should happen. - var tryLoginAfterPopupClosed = function(state) { - Meteor.apply('login', [ - {oauth: {version: 1, state: state}} - ], {wait: true}, function(error, result) { - if (error) - throw error; - - if (!result) { - // The user either closed the OAuth popup or didn't authorize - // access. Do nothing. - return; - } else { - Meteor.accounts.makeClientLoggedIn(result.id, result.token); - } - }); - }; - - var openCenteredPopup = function(url, width, height) { - var screenX = typeof window.screenX !== 'undefined' - ? window.screenX : window.screenLeft; - var screenY = typeof window.screenY !== 'undefined' - ? window.screenY : window.screenTop; - var outerWidth = typeof window.outerWidth !== 'undefined' - ? window.outerWidth : document.body.clientWidth; - var outerHeight = typeof window.outerHeight !== 'undefined' - ? window.outerHeight : (document.body.clientHeight - 22); - - // Use `outerWidth - width` and `outerHeight - height` for help in - // positioning the popup centered relative to the current window - var left = screenX + (outerWidth - width) / 2; - var top = screenY + (outerHeight - height) / 2; - var features = ('width=' + width + ',height=' + height + - ',left=' + left + ',top=' + top); - - var newwindow = window.open(url, 'Login', features); - if (newwindow.focus) - newwindow.focus(); - return newwindow; + // Include the oauth version as the last parameter + Meteor.accounts.oauth.initiateLogin(state, url, 1); }; })(); \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js index 57d67e85d3..d6934c525e 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/accounts-oauth1-helper/package.js @@ -4,7 +4,7 @@ Package.describe({ }); Package.on_use(function (api) { - api.use('accounts', ['client', 'server']); + api.use('accounts-oauth-helper', 'client'); api.use('oauth1', 'server'); api.add_files('oauth1_common.js', ['client', 'server']); diff --git a/packages/accounts-oauth2-helper/oauth2_client.js b/packages/accounts-oauth2-helper/oauth2_client.js index e6bf2ac4cf..2b0c01ee07 100644 --- a/packages/accounts-oauth2-helper/oauth2_client.js +++ b/packages/accounts-oauth2-helper/oauth2_client.js @@ -4,61 +4,7 @@ // @param state {String} The OAuth state generated by the client // @param url {String} url to page Meteor.accounts.oauth2.initiateLogin = function(state, url) { - // XXX these dimensions worked well for facebook and google, but - // it's sort of weird to have these here. Maybe an optional - // argument instead? - var popup = openCenteredPopup(url, 650, 331); - - var checkPopupOpen = setInterval(function() { - if (popup.closed) { - clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state); - } - }, 100); - }; - - // Send an OAuth login method to the server. If the user authorized - // access in the popup this should log the user in, otherwise - // nothing should happen. - var tryLoginAfterPopupClosed = function(state) { - Meteor.apply('login', [ - {oauth: {version: 2, state: state}} - ], {wait: true}, function(error, result) { - // XXX this is the wrong thing to do with the error! It should be - // delivered to the user via a callback. - if (error) - throw error; - - if (!result) { - // The user either closed the OAuth popup or didn't authorize - // access. Do nothing. - return; - } else { - Meteor.accounts.makeClientLoggedIn(result.id, result.token); - } - }); - }; - - var openCenteredPopup = function(url, width, height) { - var screenX = typeof window.screenX !== 'undefined' - ? window.screenX : window.screenLeft; - var screenY = typeof window.screenY !== 'undefined' - ? window.screenY : window.screenTop; - var outerWidth = typeof window.outerWidth !== 'undefined' - ? window.outerWidth : document.body.clientWidth; - var outerHeight = typeof window.outerHeight !== 'undefined' - ? window.outerHeight : (document.body.clientHeight - 22); - - // Use `outerWidth - width` and `outerHeight - height` for help in - // positioning the popup centered relative to the current window - var left = screenX + (outerWidth - width) / 2; - var top = screenY + (outerHeight - height) / 2; - var features = ('width=' + width + ',height=' + height + - ',left=' + left + ',top=' + top); - - var newwindow = window.open(url, 'Login', features); - if (newwindow.focus) - newwindow.focus(); - return newwindow; + // Include the oauth version as the last parameter + Meteor.accounts.oauth.initiateLogin(state, url, 2); }; })(); diff --git a/packages/accounts-oauth2-helper/package.js b/packages/accounts-oauth2-helper/package.js index a76d0bf0f6..59f8ccfb83 100644 --- a/packages/accounts-oauth2-helper/package.js +++ b/packages/accounts-oauth2-helper/package.js @@ -4,6 +4,7 @@ Package.describe({ }); Package.on_use(function (api) { + api.use('accounts-oauth-helper', 'client'); api.use('accounts', ['client', 'server']); api.add_files('oauth2_common.js', ['client', 'server']); From ca40bf2875d288953ae953bb580c724efd2e9e1f Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 01:04:56 -0400 Subject: [PATCH 14/38] better naming for oauth version variable --- packages/accounts-oauth-helper/oauth_client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 7640f1a36d..5948f73a7f 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -3,7 +3,7 @@ // // @param state {String} The OAuth state generated by the client // @param url {String} url to page - Meteor.accounts.oauth.initiateLogin = function(state, url, version) { + Meteor.accounts.oauth.initiateLogin = function(state, url, oauthVersion) { // XXX these dimensions worked well for facebook and google, but // it's sort of weird to have these here. Maybe an optional // argument instead? @@ -12,7 +12,7 @@ var checkPopupOpen = setInterval(function() { if (popup.closed) { clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state, version); + tryLoginAfterPopupClosed(state, oauthVersion); } }, 100); }; @@ -20,9 +20,9 @@ // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. - var tryLoginAfterPopupClosed = function(state, version) { + var tryLoginAfterPopupClosed = function(state, oauthVersion) { Meteor.apply('login', [ - {oauth: {version: version, state: state}} + {oauth: {oauthVersion: oauthVersion, state: state}} ], {wait: true}, function(error, result) { if (error) throw error; From 6141c032eeba1bbabc2f5ef27efab36321dcc9d1 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 01:12:20 -0400 Subject: [PATCH 15/38] remove oauth 1 and 2 client file altogether --- packages/accounts-facebook/facebook_client.js | 2 +- packages/accounts-google/google_client.js | 2 +- packages/accounts-oauth-helper/oauth_client.js | 8 ++++---- packages/accounts-oauth1-helper/oauth1_client.js | 10 ---------- packages/accounts-oauth1-helper/package.js | 1 - packages/accounts-oauth2-helper/oauth2_client.js | 10 ---------- packages/accounts-oauth2-helper/package.js | 1 - packages/accounts-twitter/twitter_client.js | 2 +- packages/accounts-weibo/weibo_client.js | 2 +- 9 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 packages/accounts-oauth1-helper/oauth1_client.js delete mode 100644 packages/accounts-oauth2-helper/oauth2_client.js diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index fabbf60bb9..9edbcfb9b9 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -17,7 +17,7 @@ '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + '&display=' + display + '&scope=' + scope + '&state=' + state; - Meteor.accounts.oauth2.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); }; })(); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 10fb1f9d7b..1c4b03c846 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -25,7 +25,7 @@ '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + '&state=' + state; - Meteor.accounts.oauth2.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); }; }) (); diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 5948f73a7f..658a5e8d96 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -3,7 +3,7 @@ // // @param state {String} The OAuth state generated by the client // @param url {String} url to page - Meteor.accounts.oauth.initiateLogin = function(state, url, oauthVersion) { + Meteor.accounts.oauth.initiateLogin = function(state, url, options) { // XXX these dimensions worked well for facebook and google, but // it's sort of weird to have these here. Maybe an optional // argument instead? @@ -12,7 +12,7 @@ var checkPopupOpen = setInterval(function() { if (popup.closed) { clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state, oauthVersion); + tryLoginAfterPopupClosed(state, options); } }, 100); }; @@ -20,9 +20,9 @@ // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. - var tryLoginAfterPopupClosed = function(state, oauthVersion) { + var tryLoginAfterPopupClosed = function(state, options) { Meteor.apply('login', [ - {oauth: {oauthVersion: oauthVersion, state: state}} + {oauth: {version: options.oauthVersion, state: state}} ], {wait: true}, function(error, result) { if (error) throw error; diff --git a/packages/accounts-oauth1-helper/oauth1_client.js b/packages/accounts-oauth1-helper/oauth1_client.js deleted file mode 100644 index f72deb3779..0000000000 --- a/packages/accounts-oauth1-helper/oauth1_client.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - // Open a popup window pointing to a OAuth1 handshake page - // - // @param state {String} The OAuth1 state generated by the client - // @param url {String} url to page - Meteor.accounts.oauth1.initiateLogin = function(state, url) { - // Include the oauth version as the last parameter - Meteor.accounts.oauth.initiateLogin(state, url, 1); - }; -})(); \ No newline at end of file diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js index d6934c525e..b97da37fb2 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/accounts-oauth1-helper/package.js @@ -9,7 +9,6 @@ Package.on_use(function (api) { api.add_files('oauth1_common.js', ['client', 'server']); api.add_files('oauth1_server.js', 'server'); - api.add_files('oauth1_client.js', 'client'); }); Package.on_test(function (api) { diff --git a/packages/accounts-oauth2-helper/oauth2_client.js b/packages/accounts-oauth2-helper/oauth2_client.js deleted file mode 100644 index 2b0c01ee07..0000000000 --- a/packages/accounts-oauth2-helper/oauth2_client.js +++ /dev/null @@ -1,10 +0,0 @@ -(function () { - // Open a popup window pointing to a OAuth handshake page - // - // @param state {String} The OAuth state generated by the client - // @param url {String} url to page - Meteor.accounts.oauth2.initiateLogin = function(state, url) { - // Include the oauth version as the last parameter - Meteor.accounts.oauth.initiateLogin(state, url, 2); - }; -})(); diff --git a/packages/accounts-oauth2-helper/package.js b/packages/accounts-oauth2-helper/package.js index 59f8ccfb83..09f09dded6 100644 --- a/packages/accounts-oauth2-helper/package.js +++ b/packages/accounts-oauth2-helper/package.js @@ -9,7 +9,6 @@ Package.on_use(function (api) { api.add_files('oauth2_common.js', ['client', 'server']); api.add_files('oauth2_server.js', 'server'); - api.add_files('oauth2_client.js', 'client'); }); Package.on_test(function (api) { diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 40d36b1482..97209b0228 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -7,7 +7,7 @@ var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) - Meteor.accounts.oauth1.initiateLogin(state, url); + Meteor.accounts.oauth.initiateLogin(state, url, { oauthVersion: 1 }); }; })(); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 040246c705..27f41fd493 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -12,7 +12,7 @@ '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + '&state=' + state; - Meteor.accounts.oauth2.initiateLogin(state, loginUrl); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); }; }) (); From 2acd9e17e3fa85682741e24cb026aab9d556f07f Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 01:40:26 -0400 Subject: [PATCH 16/38] moved oauth 1 and 2 registerService into accounts-oauth-helper --- packages/accounts-facebook/facebook_server.js | 2 +- packages/accounts-google/google_server.js | 2 +- packages/accounts-oauth-helper/package.js | 1 + .../accounts-oauth1-helper/oauth1_server.js | 22 ------------------- .../accounts-oauth1-helper/oauth1_tests.js | 2 +- .../accounts-oauth2-helper/oauth2_server.js | 21 ------------------ .../accounts-oauth2-helper/oauth2_tests.js | 2 +- packages/accounts-twitter/twitter_server.js | 2 +- packages/accounts-weibo/weibo_server.js | 2 +- 9 files changed, 7 insertions(+), 49 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 91eece0944..b80e440f6f 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -4,7 +4,7 @@ Meteor.accounts.facebook._secret = secret; }; - Meteor.accounts.oauth2.registerService('facebook', function(query) { + Meteor.accounts.oauth.registerService('facebook', 2, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 4bb726c55a..8e405fc946 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -4,7 +4,7 @@ Meteor.accounts.google._secret = secret; }; - Meteor.accounts.oauth2.registerService('google', function(query) { + Meteor.accounts.oauth.registerService('google', 2, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-oauth-helper/package.js b/packages/accounts-oauth-helper/package.js index cacb1c06b9..532b80fb16 100644 --- a/packages/accounts-oauth-helper/package.js +++ b/packages/accounts-oauth-helper/package.js @@ -8,6 +8,7 @@ Package.on_use(function (api) { api.add_files('oauth_common.js', ['client', 'server']); api.add_files('oauth_client.js', 'client'); + api.add_files('oauth_server.js', 'server'); }); Package.on_test(function (api) { diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 5fb0cf1021..818d712dfc 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -3,28 +3,6 @@ Meteor.accounts.oauth1._services = {}; - // Register a handler for an OAuth1 service. The handler will be called - // when we get an incoming http request on /_oauth/{serviceName}. This - // handler should use that information to fetch data about the user - // logging in. - // - // @param name {String} e.g. "flickr", "twitter" - // @param handleOauthRequest {Function(query)} - // - query is an object with the parameters passed in the query string - // - return value is: - // - {options: (options), extra: (optional extra)} (same as the - // arguments to Meteor.accounts.updateOrCreateUser) - // - `null` if the user declined to give permissions - // XXX In the context of oauth1 the name handleOauthRequest doesn't make as much sense - Meteor.accounts.oauth1.registerService = function (name, handleOauthRequest) { - if (Meteor.accounts.oauth1._services[name]) - throw new Error("Already registered the " + name + " OAuth1 service"); - - Meteor.accounts.oauth1._services[name] = { - handleOauthRequest: handleOauthRequest - }; - }; - // Listen to calls to `login` with an oauth option set Meteor.accounts.registerLoginHandler(function (options) { if (!options.oauth || options.oauth.version !== 1) diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index afa341dcb4..8b87ce0b70 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -7,7 +7,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth1._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth1.registerService("foobook", function (query) { + Meteor.accounts.oauth.registerService("foobook", 1, function (query) { return { options: { email: email, diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 6853951020..169631e87d 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -3,27 +3,6 @@ Meteor.accounts.oauth2._services = {}; - // Register a handler for an OAuth2 service. The handler will be called - // when we get an incoming http request on /_oauth/{serviceName}. This - // handler should use that information to fetch data about the user - // logging in. - // - // @param name {String} e.g. "google", "facebook" - // @param handleOauthRequest {Function(query)} - // - query is an object with the parameters passed in the query string - // - return value is: - // - {options: (options), extra: (optional extra)} (same as the - // arguments to Meteor.accounts.updateOrCreateUser) - // - `null` if the user declined to give permissions - Meteor.accounts.oauth2.registerService = function (name, handleOauthRequest) { - if (Meteor.accounts.oauth2._services[name]) - throw new Error("Already registered the " + name + " OAuth2 service"); - - Meteor.accounts.oauth2._services[name] = { - handleOauthRequest: handleOauthRequest - }; - }; - // Listen to calls to `login` with an oauth option set Meteor.accounts.registerLoginHandler(function (options) { if (!options.oauth || options.oauth.version !== 2) diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index 5d119e4060..f95e0ac783 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -10,7 +10,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth2._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth2.registerService("foobook", function (query) { + Meteor.accounts.oauth.registerService("foobook", 2, function (query) { return { options: { email: email, diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 23d35575b2..65c26bcd05 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -4,7 +4,7 @@ Meteor.accounts.twitter._secret = secret; }; - Meteor.accounts.oauth1.registerService('twitter', function(oauth) { + Meteor.accounts.oauth.registerService('twitter', 1, function(oauth) { var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index a50908957a..f0738bd676 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -4,7 +4,7 @@ Meteor.accounts.weibo._secret = secret; }; - Meteor.accounts.oauth2.registerService('weibo', function(query) { + Meteor.accounts.oauth.registerService('weibo', 2, function(query) { if (query.error) { // The user didn't authorize access return null; From 19f043583d8d7159e6ca7a842b20e33ea6ff0a76 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 01:53:31 -0400 Subject: [PATCH 17/38] moved common server side oauth setup code to accounts-oauth-helpers --- .../accounts-oauth1-helper/oauth1_server.js | 21 +-------------- .../accounts-oauth2-helper/oauth2_server.js | 26 ++----------------- 2 files changed, 3 insertions(+), 44 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 818d712dfc..4cf6ea0082 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -3,26 +3,7 @@ Meteor.accounts.oauth1._services = {}; - // Listen to calls to `login` with an oauth option set - Meteor.accounts.registerLoginHandler(function (options) { - if (!options.oauth || options.oauth.version !== 1) - return undefined; // don't handle - - var result = Meteor.accounts.oauth1._loginResultForState[options.oauth.state]; - if (result === undefined) // not using `!result` since can be null - // We weren't notified of the user authorizing the login. - return null; - else - return result; - }); - - // When we get an incoming OAuth http request we complete the oauth - // handshake, account and token setup before responding. The - // results are stored in this map which is then read when the login - // method is called. Maps state --> return value of `login` - // - // XXX we should periodically clear old entries - Meteor.accounts.oauth1._loginResultForState = {}; + Meteor.accounts.oauth._setup({oauthVersion: 1}); // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 169631e87d..620c2d0aa9 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -3,33 +3,11 @@ Meteor.accounts.oauth2._services = {}; - // Listen to calls to `login` with an oauth option set - Meteor.accounts.registerLoginHandler(function (options) { - if (!options.oauth || options.oauth.version !== 2) - return undefined; // don't handle - - var result = Meteor.accounts.oauth2._loginResultForState[options.oauth.state]; - if (result === undefined) // not using `!result` since can be null - // We weren't notified of the user authorizing the login. - return null; - else if (result instanceof Error) - // We tried to login, but there was a fatal error. Report it back - // to the user. - throw result; - else - return result; - }); - - // When we get an incoming OAuth http request we complete the oauth - // handshake, account and token setup before responding. The - // results are stored in this map which is then read when the login - // method is called. Maps state --> return value of `login` - // - // XXX we should periodically clear old entries - Meteor.accounts.oauth2._loginResultForState = {}; + Meteor.accounts.oauth._setup({oauthVersion: 2}); // connect middleware Meteor.accounts.oauth2._handleRequest = function (req, res, next) { + // req.url will be "/_oauth/?" // NOTE: query param is mandatory. var barePath = req.url.substring(0, req.url.indexOf('?')); From 30b6b314484d6a32fd3d069c0fb3e3a9946d868d Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 02:07:20 -0400 Subject: [PATCH 18/38] pass oauth version into registerService as part of an options obj --- packages/accounts-facebook/facebook_server.js | 2 +- packages/accounts-google/google_server.js | 2 +- .../accounts-oauth-helper/oauth_server.js | 52 +++++++++++++++++++ .../accounts-oauth1-helper/oauth1_tests.js | 2 +- .../accounts-oauth2-helper/oauth2_tests.js | 2 +- packages/accounts-twitter/twitter_server.js | 2 +- packages/accounts-weibo/weibo_server.js | 2 +- 7 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 packages/accounts-oauth-helper/oauth_server.js diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index b80e440f6f..4ced8c6e2c 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -4,7 +4,7 @@ Meteor.accounts.facebook._secret = secret; }; - Meteor.accounts.oauth.registerService('facebook', 2, function(query) { + Meteor.accounts.oauth.registerService('facebook', {oauthVersion: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 8e405fc946..74115f6d62 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -4,7 +4,7 @@ Meteor.accounts.google._secret = secret; }; - Meteor.accounts.oauth.registerService('google', 2, function(query) { + Meteor.accounts.oauth.registerService('google', {oauthVersion: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js new file mode 100644 index 0000000000..f0c9909a16 --- /dev/null +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -0,0 +1,52 @@ +(function () { + + // Register a handler for an OAuth service. The handler will be called + // when we get an incoming http request on /_oauth/{serviceName}. This + // handler should use that information to fetch data about the user + // logging in. + // + // @param name {String} e.g. "google", "facebook" + // @param handleOauthRequest {Function(query)} + // - query is an object with the parameters passed in the query string + // - return value is: + // - {options: (options), extra: (optional extra)} (same as the + // arguments to Meteor.accounts.updateOrCreateUser) + // - `null` if the user declined to give permissions + Meteor.accounts.oauth.registerService = function (name, options, handleOauthRequest) { + var oauthAccounts = Meteor.accounts['oauth' + options.oauthVersion]; + + if (oauthAccounts._services[name]) + throw new Error("Already registered the " + name + " OAuth" + options.oauthVersion + " service"); + + oauthAccounts._services[name] = { + handleOauthRequest: handleOauthRequest + }; + }; + + Meteor.accounts.oauth._setup = function(setupOptions) { + var oauthAccounts = Meteor.accounts['oauth' + setupOptions.oauthVersion]; + + // Listen to calls to `login` with an oauth option set + Meteor.accounts.registerLoginHandler(function (options) { + if (!options.oauth || options.oauth.version !== setupOptions.oauthVersion) + return undefined; // don't handle + + var result = oauthAccounts._loginResultForState[options.oauth.state]; + if (result === undefined) // not using `!result` since can be null + // We weren't notified of the user authorizing the login. + return null; + else + return result; + }); + + // When we get an incoming OAuth http request we complete the oauth + // handshake, account and token setup before responding. The + // results are stored in this map which is then read when the login + // method is called. Maps state --> return value of `login` + // + // XXX we should periodically clear old entries + oauthAccounts._loginResultForState = {}; + + }; + +})(); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 8b87ce0b70..1ae9f1bf0f 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -7,7 +7,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth1._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", 1, function (query) { + Meteor.accounts.oauth.registerService("foobook", {oauthVersion: 1}, function (query) { return { options: { email: email, diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index f95e0ac783..d6bd124899 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -10,7 +10,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth2._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", 2, function (query) { + Meteor.accounts.oauth.registerService("foobook", {oauthVersion: 2}, function (query) { return { options: { email: email, diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 65c26bcd05..854cad60ef 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -4,7 +4,7 @@ Meteor.accounts.twitter._secret = secret; }; - Meteor.accounts.oauth.registerService('twitter', 1, function(oauth) { + Meteor.accounts.oauth.registerService('twitter', {oauthVersion: 1}, function(oauth) { var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index f0738bd676..b0d061c802 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -4,7 +4,7 @@ Meteor.accounts.weibo._secret = secret; }; - Meteor.accounts.oauth.registerService('weibo', 2, function(query) { + Meteor.accounts.oauth.registerService('weibo', {oauthVersion: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; From bba4685269b0dc675c644a4732f08034a95a88ef Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 02:11:13 -0400 Subject: [PATCH 19/38] changed oauthVersion to version --- packages/accounts-facebook/facebook_client.js | 2 +- packages/accounts-facebook/facebook_server.js | 2 +- packages/accounts-google/google_client.js | 2 +- packages/accounts-google/google_server.js | 2 +- packages/accounts-oauth-helper/oauth_client.js | 2 +- packages/accounts-oauth-helper/oauth_server.js | 8 ++++---- packages/accounts-oauth1-helper/oauth1_server.js | 2 +- packages/accounts-oauth1-helper/oauth1_tests.js | 2 +- packages/accounts-oauth2-helper/oauth2_server.js | 2 +- packages/accounts-oauth2-helper/oauth2_tests.js | 2 +- packages/accounts-twitter/twitter_client.js | 2 +- packages/accounts-twitter/twitter_server.js | 2 +- packages/accounts-weibo/weibo_client.js | 2 +- packages/accounts-weibo/weibo_server.js | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 9edbcfb9b9..420717d5e8 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -17,7 +17,7 @@ '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + '&display=' + display + '&scope=' + scope + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); }; })(); diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 4ced8c6e2c..b625595a44 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -4,7 +4,7 @@ Meteor.accounts.facebook._secret = secret; }; - Meteor.accounts.oauth.registerService('facebook', {oauthVersion: 2}, function(query) { + Meteor.accounts.oauth.registerService('facebook', {version: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 1c4b03c846..8bab1862ff 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -25,7 +25,7 @@ '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); }; }) (); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 74115f6d62..94037959c2 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -4,7 +4,7 @@ Meteor.accounts.google._secret = secret; }; - Meteor.accounts.oauth.registerService('google', {oauthVersion: 2}, function(query) { + Meteor.accounts.oauth.registerService('google', {version: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index 658a5e8d96..f2e1ce9654 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -22,7 +22,7 @@ // nothing should happen. var tryLoginAfterPopupClosed = function(state, options) { Meteor.apply('login', [ - {oauth: {version: options.oauthVersion, state: state}} + {oauth: {version: options.version, state: state}} ], {wait: true}, function(error, result) { if (error) throw error; diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index f0c9909a16..c91a5af6ea 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -13,10 +13,10 @@ // arguments to Meteor.accounts.updateOrCreateUser) // - `null` if the user declined to give permissions Meteor.accounts.oauth.registerService = function (name, options, handleOauthRequest) { - var oauthAccounts = Meteor.accounts['oauth' + options.oauthVersion]; + var oauthAccounts = Meteor.accounts['oauth' + options.version]; if (oauthAccounts._services[name]) - throw new Error("Already registered the " + name + " OAuth" + options.oauthVersion + " service"); + throw new Error("Already registered the " + name + " OAuth" + options.version + " service"); oauthAccounts._services[name] = { handleOauthRequest: handleOauthRequest @@ -24,11 +24,11 @@ }; Meteor.accounts.oauth._setup = function(setupOptions) { - var oauthAccounts = Meteor.accounts['oauth' + setupOptions.oauthVersion]; + var oauthAccounts = Meteor.accounts['oauth' + setupOptions.version]; // Listen to calls to `login` with an oauth option set Meteor.accounts.registerLoginHandler(function (options) { - if (!options.oauth || options.oauth.version !== setupOptions.oauthVersion) + if (!options.oauth || options.oauth.version !== setupOptions.version) return undefined; // don't handle var result = oauthAccounts._loginResultForState[options.oauth.state]; diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 4cf6ea0082..71b139632c 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -3,7 +3,7 @@ Meteor.accounts.oauth1._services = {}; - Meteor.accounts.oauth._setup({oauthVersion: 1}); + Meteor.accounts.oauth._setup({version: 1}); // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 1ae9f1bf0f..0a5e5774a7 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -7,7 +7,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth1._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", {oauthVersion: 1}, function (query) { + Meteor.accounts.oauth.registerService("foobook", {version: 1}, function (query) { return { options: { email: email, diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 620c2d0aa9..566ab3f978 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -3,7 +3,7 @@ Meteor.accounts.oauth2._services = {}; - Meteor.accounts.oauth._setup({oauthVersion: 2}); + Meteor.accounts.oauth._setup({version: 2}); // connect middleware Meteor.accounts.oauth2._handleRequest = function (req, res, next) { diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index d6bd124899..b1b290c2c1 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -10,7 +10,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth2._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", {oauthVersion: 2}, function (query) { + Meteor.accounts.oauth.registerService("foobook", {version: 2}, function (query) { return { options: { email: email, diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 97209b0228..38a33011e1 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -7,7 +7,7 @@ var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) - Meteor.accounts.oauth.initiateLogin(state, url, { oauthVersion: 1 }); + Meteor.accounts.oauth.initiateLogin(state, url, { version: 1 }); }; })(); diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 854cad60ef..326ac8d911 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -4,7 +4,7 @@ Meteor.accounts.twitter._secret = secret; }; - Meteor.accounts.oauth.registerService('twitter', {oauthVersion: 1}, function(oauth) { + Meteor.accounts.oauth.registerService('twitter', {version: 1}, function(oauth) { var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 27f41fd493..7855f9be1c 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -12,7 +12,7 @@ '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { oauthVersion: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); }; }) (); diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index b0d061c802..1a339c1837 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -4,7 +4,7 @@ Meteor.accounts.weibo._secret = secret; }; - Meteor.accounts.oauth.registerService('weibo', {oauthVersion: 2}, function(query) { + Meteor.accounts.oauth.registerService('weibo', {version: 2}, function(query) { if (query.error) { // The user didn't authorize access return null; From d7b2278630313ca55e5b2ac5f7a1da2f6518572d Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 02:19:22 -0400 Subject: [PATCH 20/38] changed variable name for consistency --- packages/accounts-weibo/weibo_server.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 1a339c1837..6b479e0d51 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -15,15 +15,16 @@ if (!Meteor.accounts.weibo._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.setSecret first"); - var result = getAccessToken(query); - var identity = getIdentity(result.access_token, parseInt(result.uid, 10)); + var accessToken = getAccessToken(query); + var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10)); return { options: { services: { weibo: { - id: result.uid, - accessToken: result.accessToken + id: accessToken.uid, + accessToken: accessToken.accessToken, + screenName: identity.screen_name } } }, From 768af064ba5a0b0f672d9b7aafafb5e9c8dc3d39 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 02:33:03 -0400 Subject: [PATCH 21/38] use appId instead of clientId to accommodate more cleanup --- packages/accounts-google/google_client.js | 4 ++-- packages/accounts-google/google_common.js | 2 +- packages/accounts-google/google_server.js | 4 ++-- packages/accounts-weibo/weibo_client.js | 4 ++-- packages/accounts-weibo/weibo_common.js | 2 +- packages/accounts-weibo/weibo_server.js | 4 ++-- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 8bab1862ff..099578bc98 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,6 +1,6 @@ (function () { Meteor.loginWithGoogle = function () { - if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl) + if (!Meteor.accounts.google._appId || !Meteor.accounts.google._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first"); var state = Meteor.uuid(); @@ -20,7 +20,7 @@ var loginUrl = 'https://accounts.google.com/o/oauth2/auth' + '?response_type=code' + - '&client_id=' + Meteor.accounts.google._clientId + + '&client_id=' + Meteor.accounts.google._appId + '&scope=' + flat_scope + '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + '&state=' + state; diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js index 5f6e4ddeaf..7007fe7208 100644 --- a/packages/accounts-google/google_common.js +++ b/packages/accounts-google/google_common.js @@ -3,7 +3,7 @@ if (!Meteor.accounts.google) { } Meteor.accounts.google.config = function(clientId, appUrl, options) { - Meteor.accounts.google._clientId = clientId; + Meteor.accounts.google._appId = clientId; Meteor.accounts.google._appUrl = appUrl; Meteor.accounts.google._options = options; }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 94037959c2..203f607530 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -10,7 +10,7 @@ return null; } - if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl) + if (!Meteor.accounts.google._appId || !Meteor.accounts.google._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first"); if (!Meteor.accounts.google._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.setSecret first"); @@ -31,7 +31,7 @@ var result = Meteor.http.post( "https://accounts.google.com/o/oauth2/token", {params: { code: query.code, - client_id: Meteor.accounts.google._clientId, + client_id: Meteor.accounts.google._appId, client_secret: Meteor.accounts.google._secret, redirect_uri: Meteor.accounts.google._appUrl + "/_oauth/google?close", grant_type: 'authorization_code' diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 7855f9be1c..017f87f8bb 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,6 +1,6 @@ (function () { Meteor.loginWithWeibo = function () { - if (!Meteor.accounts.weibo._clientId || !Meteor.accounts.weibo._appUrl) + if (!Meteor.accounts.weibo._appId || !Meteor.accounts.weibo._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first"); var state = Meteor.uuid(); @@ -8,7 +8,7 @@ var loginUrl = 'https://api.weibo.com/oauth2/authorize' + '?response_type=code' + - '&client_id=' + Meteor.accounts.weibo._clientId + + '&client_id=' + Meteor.accounts.weibo._appId + '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + '&state=' + state; diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js index 940750acb4..3b05f79258 100644 --- a/packages/accounts-weibo/weibo_common.js +++ b/packages/accounts-weibo/weibo_common.js @@ -3,6 +3,6 @@ if (!Meteor.accounts.weibo) { } Meteor.accounts.weibo.config = function(clientId, appUrl) { - Meteor.accounts.weibo._clientId = clientId; + Meteor.accounts.weibo._appId = clientId; Meteor.accounts.weibo._appUrl = appUrl; }; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 6b479e0d51..64e1e5a3d2 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -10,7 +10,7 @@ return null; } - if (!Meteor.accounts.weibo._clientId || !Meteor.accounts.weibo._appUrl) + if (!Meteor.accounts.weibo._appId || !Meteor.accounts.weibo._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first"); if (!Meteor.accounts.weibo._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.setSecret first"); @@ -36,7 +36,7 @@ var result = Meteor.http.post( "https://api.weibo.com/oauth2/access_token", {params: { code: query.code, - client_id: Meteor.accounts.weibo._clientId, + client_id: Meteor.accounts.weibo._appId, client_secret: Meteor.accounts.weibo._secret, redirect_uri: Meteor.accounts.weibo._appUrl + "/_oauth/weibo?close", grant_type: 'authorization_code' From 380910d72f18345409bb72d1fb0b8ab199e0b5a1 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 03:00:32 -0400 Subject: [PATCH 22/38] move common code from oauth2 providers up into _requestHandler --- packages/accounts-facebook/facebook_server.js | 9 --------- packages/accounts-google/google_server.js | 9 --------- packages/accounts-oauth1-helper/oauth1_server.js | 8 ++++---- packages/accounts-oauth2-helper/oauth2_server.js | 11 +++++++++++ packages/accounts-weibo/weibo_server.js | 9 --------- 5 files changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index b625595a44..e7d521ca6f 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -5,15 +5,6 @@ }; Meteor.accounts.oauth.registerService('facebook', {version: 2}, function(query) { - if (query.error) { - // The user didn't authorize access - return null; - } - - if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.config first"); - if (!Meteor.accounts.facebook._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setSecret first"); var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 203f607530..2fd5b350ea 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -5,15 +5,6 @@ }; Meteor.accounts.oauth.registerService('google', {version: 2}, function(query) { - if (query.error) { - // The user didn't authorize access - return null; - } - - if (!Meteor.accounts.google._appId || !Meteor.accounts.google._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first"); - if (!Meteor.accounts.google._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.setSecret first"); var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 71b139632c..23388327bf 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -23,16 +23,16 @@ return; } - // Make sure we prepare the login results before returning. - // This way the subsequent call to the `login` method will be - // immediate. - // Make sure we're configured if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); if (!Meteor.accounts[serviceName]._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + // Make sure we prepare the login results before returning. + // This way the subsequent call to the `login` method will be + // immediate. + var config = Meteor.accounts[serviceName]; var oauth = new OAuth1(config); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 566ab3f978..183ad442af 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -24,6 +24,17 @@ return; } + // Make sure we're configured + if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); + if (!Meteor.accounts[serviceName]._secret) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + + if (req.query.error) { + // The user didn't authorize access + return null; + } + // Make sure we prepare the login results before returning. // This way the subsequent call to the `login` method will be // immediate. diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 64e1e5a3d2..34bceeb3da 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -5,15 +5,6 @@ }; Meteor.accounts.oauth.registerService('weibo', {version: 2}, function(query) { - if (query.error) { - // The user didn't authorize access - return null; - } - - if (!Meteor.accounts.weibo._appId || !Meteor.accounts.weibo._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first"); - if (!Meteor.accounts.weibo._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.setSecret first"); var accessToken = getAccessToken(query); var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10)); From 2259ea6e08dee43001cf7a98d2a28ee06d28886a Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Thu, 2 Aug 2012 04:18:45 -0400 Subject: [PATCH 23/38] move common middleware functionality into accounts-oauth-helper middleware --- .../accounts-oauth-helper/oauth_server.js | 42 +++++++++++++++++++ .../accounts-oauth1-helper/oauth1_server.js | 33 +++------------ .../accounts-oauth2-helper/oauth2_server.js | 32 ++------------ 3 files changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index c91a5af6ea..3e7cd8c915 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -1,4 +1,5 @@ (function () { + var connect = __meteor_bootstrap__.require("connect"); // Register a handler for an OAuth service. The handler will be called // when we get an incoming http request on /_oauth/{serviceName}. This @@ -49,4 +50,45 @@ }; + // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware + Meteor.accounts.oauth._handleRequest = function (req, res, next) { + + // req.url will be "/_oauth/?" + var barePath = req.url.substring(0, req.url.indexOf('?')); + var splitPath = barePath.split('/'); + + // Find service based on url + var serviceName = req._serviceName = splitPath[2]; + + // Any non-oauth request will continue down the default middlewares + // Same goes for service that hasn't been registered + if (splitPath[1] !== '_oauth') { + next(); + return; + } + + // Make sure we're configured + if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); + if (!Meteor.accounts[serviceName]._secret) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + + next(); + }; + + Meteor.accounts.oauth._loadMiddleWare = function(middleware) { + __meteor_bootstrap__.app + .use(connect.query()) + .use(function(req, res, next) { + // Need to create a Fiber since we're using synchronous http + // calls and nothing else is wrapping this in a fiber + // automatically + Fiber(function () { + middleware(req, res, next); + }).run(); + }); + }; + + Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth._handleRequest); + })(); diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 23388327bf..326fe7f3a2 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -8,32 +8,19 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { - // req.url will be "/_oauth/?" - var barePath = req.url.substring(0, req.url.indexOf('?')); - var splitPath = barePath.split('/'); + var service = Meteor.accounts.oauth1._services[req._serviceName]; - // Find service based on url - var serviceName = splitPath[2]; - var service = Meteor.accounts.oauth1._services[serviceName]; - - // Any non-oauth request will continue down the default middlewares - // Same goes for service that hasn't been registered - if (splitPath[1] !== '_oauth' || !service) { + // Skip everything if there's no service set by the oauth middleware + if (!service) { next(); return; } - // Make sure we're configured - if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); - if (!Meteor.accounts[serviceName]._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); - // Make sure we prepare the login results before returning. // This way the subsequent call to the `login` method will be // immediate. - var config = Meteor.accounts[serviceName]; + var config = Meteor.accounts[req._serviceName]; var oauth = new OAuth1(config); // If we get here with a callback url we need a request token to @@ -96,16 +83,6 @@ } }; - // Listen on /_oauth/* - __meteor_bootstrap__.app - .use(connect.query()) - .use(function(req, res, next) { - // Need to create a Fiber since we're using synchronous http - // calls and nothing else is wrapping this in a fiber - // automatically - Fiber(function () { - Meteor.accounts.oauth1._handleRequest(req, res, next); - }).run(); - }); + Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth1._handleRequest); })(); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 183ad442af..923d3efa32 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -8,28 +8,14 @@ // connect middleware Meteor.accounts.oauth2._handleRequest = function (req, res, next) { - // req.url will be "/_oauth/?" - // NOTE: query param is mandatory. - var barePath = req.url.substring(0, req.url.indexOf('?')); - var splitPath = barePath.split('/'); + var service = Meteor.accounts.oauth2._services[req._serviceName]; - // Find service based on url - var serviceName = splitPath[2]; - var service = Meteor.accounts.oauth2._services[serviceName]; - - // Any non-oauth request will continue down the default middlewares - // Same goes for service that hasn't been registered - if (splitPath[1] !== '_oauth' || !service) { + // Skip everything if there's no service set by the oauth middleware + if (!service) { next(); return; } - // Make sure we're configured - if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); - if (!Meteor.accounts[serviceName]._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); - if (req.query.error) { // The user didn't authorize access return null; @@ -71,16 +57,6 @@ } }; - // Listen on /_oauth/* - __meteor_bootstrap__.app - .use(connect.query()) - .use(function(req, res, next) { - // Need to create a Fiber since we're using synchronous http - // calls and nothing else is wrapping this in a fiber - // automatically - Fiber(function () { - Meteor.accounts.oauth2._handleRequest(req, res, next); - }).run(); - }); + Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth2._handleRequest); })(); From e14e7b7b82a7cd40210b5a5f63d6d604e262230a Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 14 Aug 2012 01:36:25 -0400 Subject: [PATCH 24/38] restore sanity to oauth req handlers --- packages/accounts-oauth-helper/oauth_server.js | 9 +++------ packages/accounts-oauth1-helper/oauth1_server.js | 5 +++-- packages/accounts-oauth2-helper/oauth2_server.js | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 3e7cd8c915..c9e4a69315 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -51,19 +51,18 @@ }; // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware - Meteor.accounts.oauth._handleRequest = function (req, res, next) { + Meteor.accounts.oauth._prepareRequest = function (req) { // req.url will be "/_oauth/?" var barePath = req.url.substring(0, req.url.indexOf('?')); var splitPath = barePath.split('/'); // Find service based on url - var serviceName = req._serviceName = splitPath[2]; + var serviceName = splitPath[2]; // Any non-oauth request will continue down the default middlewares // Same goes for service that hasn't been registered if (splitPath[1] !== '_oauth') { - next(); return; } @@ -73,7 +72,7 @@ if (!Meteor.accounts[serviceName]._secret) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); - next(); + return serviceName; }; Meteor.accounts.oauth._loadMiddleWare = function(middleware) { @@ -89,6 +88,4 @@ }); }; - Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth._handleRequest); - })(); diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 326fe7f3a2..2ae94659d7 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -8,7 +8,8 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { - var service = Meteor.accounts.oauth1._services[req._serviceName]; + var serviceName = Meteor.accounts.oauth._prepareRequest(req); + var service = Meteor.accounts.oauth1._services[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) { @@ -20,7 +21,7 @@ // This way the subsequent call to the `login` method will be // immediate. - var config = Meteor.accounts[req._serviceName]; + var config = Meteor.accounts[serviceName]; var oauth = new OAuth1(config); // If we get here with a callback url we need a request token to diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 923d3efa32..1d61a69184 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -8,7 +8,8 @@ // connect middleware Meteor.accounts.oauth2._handleRequest = function (req, res, next) { - var service = Meteor.accounts.oauth2._services[req._serviceName]; + var serviceName = Meteor.accounts.oauth._prepareRequest(req); + var service = Meteor.accounts.oauth2._services[serviceName]; // Skip everything if there's no service set by the oauth middleware if (!service) { From a352ff25246c69862b480d826f93918d93313450 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 14 Aug 2012 02:21:23 -0400 Subject: [PATCH 25/38] move oauth1 signing code into accounts-oauth1-helper --- packages/{oauth1 => accounts-oauth1-helper}/oauth1.js | 0 packages/accounts-oauth1-helper/package.js | 2 +- packages/oauth1/package.js | 11 ----------- 3 files changed, 1 insertion(+), 12 deletions(-) rename packages/{oauth1 => accounts-oauth1-helper}/oauth1.js (100%) delete mode 100644 packages/oauth1/package.js diff --git a/packages/oauth1/oauth1.js b/packages/accounts-oauth1-helper/oauth1.js similarity index 100% rename from packages/oauth1/oauth1.js rename to packages/accounts-oauth1-helper/oauth1.js diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js index b97da37fb2..a42b9b1805 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/accounts-oauth1-helper/package.js @@ -5,8 +5,8 @@ Package.describe({ Package.on_use(function (api) { api.use('accounts-oauth-helper', 'client'); - api.use('oauth1', 'server'); + api.add_files('oauth1.js', 'server'); api.add_files('oauth1_common.js', ['client', 'server']); api.add_files('oauth1_server.js', 'server'); }); diff --git a/packages/oauth1/package.js b/packages/oauth1/package.js deleted file mode 100644 index a7afe9424e..0000000000 --- a/packages/oauth1/package.js +++ /dev/null @@ -1,11 +0,0 @@ -Package.describe({ - summary: "Code for oauth1 clients", -}); - -Package.on_use(function (api) { - api.add_files('oauth1.js', 'server'); -}); - -Package.on_test(function (api) { - // XXX Add some! -}); From 56399c27559355207e7335c929f056d8a2a67cf7 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 14 Aug 2012 03:39:51 -0400 Subject: [PATCH 26/38] refactoring and reverting back to using clientId internally for weibo and google --- packages/accounts-facebook/facebook_common.js | 1 + packages/accounts-google/google_client.js | 4 ++-- packages/accounts-google/google_common.js | 3 ++- packages/accounts-google/google_server.js | 2 +- .../accounts-oauth-helper/oauth_server.js | 22 +++++++++++++------ .../accounts-oauth1-helper/oauth1_server.js | 5 ++++- .../accounts-oauth2-helper/oauth2_server.js | 5 ++++- packages/accounts-twitter/twitter_common.js | 1 + packages/accounts-weibo/weibo_client.js | 4 ++-- packages/accounts-weibo/weibo_common.js | 3 ++- packages/accounts-weibo/weibo_server.js | 2 +- 11 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js index 8fd6105955..e1697e3f5a 100644 --- a/packages/accounts-facebook/facebook_common.js +++ b/packages/accounts-facebook/facebook_common.js @@ -1,5 +1,6 @@ if (!Meteor.accounts.facebook) { Meteor.accounts.facebook = {}; + Meteor.accounts.facebook._requireConfigs = ['appId', 'appUrl']; } Meteor.accounts.facebook.config = function(appId, appUrl, options) { diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 099578bc98..8bab1862ff 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,6 +1,6 @@ (function () { Meteor.loginWithGoogle = function () { - if (!Meteor.accounts.google._appId || !Meteor.accounts.google._appUrl) + if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first"); var state = Meteor.uuid(); @@ -20,7 +20,7 @@ var loginUrl = 'https://accounts.google.com/o/oauth2/auth' + '?response_type=code' + - '&client_id=' + Meteor.accounts.google._appId + + '&client_id=' + Meteor.accounts.google._clientId + '&scope=' + flat_scope + '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + '&state=' + state; diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js index 7007fe7208..f53669a543 100644 --- a/packages/accounts-google/google_common.js +++ b/packages/accounts-google/google_common.js @@ -1,9 +1,10 @@ if (!Meteor.accounts.google) { Meteor.accounts.google = {}; + Meteor.accounts.google._requireConfigs = ['clientId', 'appUrl']; } Meteor.accounts.google.config = function(clientId, appUrl, options) { - Meteor.accounts.google._appId = clientId; + Meteor.accounts.google._clientId = clientId; Meteor.accounts.google._appUrl = appUrl; Meteor.accounts.google._options = options; }; diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 2fd5b350ea..49f6cd36ff 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -22,7 +22,7 @@ var result = Meteor.http.post( "https://accounts.google.com/o/oauth2/token", {params: { code: query.code, - client_id: Meteor.accounts.google._appId, + client_id: Meteor.accounts.google._clientId, client_secret: Meteor.accounts.google._secret, redirect_uri: Meteor.accounts.google._appUrl + "/_oauth/google?close", grant_type: 'authorization_code' diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index c9e4a69315..ef4c9ee231 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -51,7 +51,7 @@ }; // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware - Meteor.accounts.oauth._prepareRequest = function (req) { + Meteor.accounts.oauth._requestServiceName = function (req) { // req.url will be "/_oauth/?" var barePath = req.url.substring(0, req.url.indexOf('?')); @@ -66,15 +66,23 @@ return; } - // Make sure we're configured - if (!Meteor.accounts[serviceName]._appId || !Meteor.accounts[serviceName]._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); - if (!Meteor.accounts[serviceName]._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); - return serviceName; }; + // Make sure we're configured + Meteor.accounts.oauth._ensureConfigured = function(serviceName) { + var service = Meteor.accounts[serviceName]; + + _.each(Meteor.accounts[serviceName]._requireConfigs, function(key) { + var configKey = '_' + key; + if (!service[configKey]) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); + }); + + if (!service._secret) + throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + }; + Meteor.accounts.oauth._loadMiddleWare = function(middleware) { __meteor_bootstrap__.app .use(connect.query()) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 2ae94659d7..8eb860e598 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -8,7 +8,7 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (req, res, next) { - var serviceName = Meteor.accounts.oauth._prepareRequest(req); + var serviceName = Meteor.accounts.oauth._requestServiceName(req); var service = Meteor.accounts.oauth1._services[serviceName]; // Skip everything if there's no service set by the oauth middleware @@ -17,6 +17,9 @@ return; } + // Make sure we're configured + Meteor.accounts.oauth._ensureConfigured(serviceName); + // Make sure we prepare the login results before returning. // This way the subsequent call to the `login` method will be // immediate. diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 1d61a69184..3e5254c69a 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -8,7 +8,7 @@ // connect middleware Meteor.accounts.oauth2._handleRequest = function (req, res, next) { - var serviceName = Meteor.accounts.oauth._prepareRequest(req); + var serviceName = Meteor.accounts.oauth._requestServiceName(req); var service = Meteor.accounts.oauth2._services[serviceName]; // Skip everything if there's no service set by the oauth middleware @@ -17,6 +17,9 @@ return; } + // Make sure we're configured + Meteor.accounts.oauth._ensureConfigured(serviceName); + if (req.query.error) { // The user didn't authorize access return null; diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index 62f4d8d373..e38e8124fa 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,5 +1,6 @@ if (!Meteor.accounts.twitter) { Meteor.accounts.twitter = {}; + Meteor.accounts.twitter._requireConfigs = ['appId', 'appUrl']; } Meteor.accounts.twitter.config = function(appId, appUrl, options) { diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 017f87f8bb..7855f9be1c 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,6 +1,6 @@ (function () { Meteor.loginWithWeibo = function () { - if (!Meteor.accounts.weibo._appId || !Meteor.accounts.weibo._appUrl) + if (!Meteor.accounts.weibo._clientId || !Meteor.accounts.weibo._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first"); var state = Meteor.uuid(); @@ -8,7 +8,7 @@ var loginUrl = 'https://api.weibo.com/oauth2/authorize' + '?response_type=code' + - '&client_id=' + Meteor.accounts.weibo._appId + + '&client_id=' + Meteor.accounts.weibo._clientId + '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + '&state=' + state; diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js index 3b05f79258..6c21d9bffc 100644 --- a/packages/accounts-weibo/weibo_common.js +++ b/packages/accounts-weibo/weibo_common.js @@ -1,8 +1,9 @@ if (!Meteor.accounts.weibo) { Meteor.accounts.weibo = {}; + Meteor.accounts.weibo._requireConfigs = ['clientId', 'appUrl']; } Meteor.accounts.weibo.config = function(clientId, appUrl) { - Meteor.accounts.weibo._appId = clientId; + Meteor.accounts.weibo._clientId = clientId; Meteor.accounts.weibo._appUrl = appUrl; }; diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 34bceeb3da..034ade041d 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -27,7 +27,7 @@ var result = Meteor.http.post( "https://api.weibo.com/oauth2/access_token", {params: { code: query.code, - client_id: Meteor.accounts.weibo._appId, + client_id: Meteor.accounts.weibo._clientId, client_secret: Meteor.accounts.weibo._secret, redirect_uri: Meteor.accounts.weibo._appUrl + "/_oauth/weibo?close", grant_type: 'authorization_code' From 6b914593aeeb6878c02ca8cd745256a69b5da8e7 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Wed, 15 Aug 2012 21:05:12 -0400 Subject: [PATCH 27/38] record oauth1 access token secret --- packages/accounts-twitter/twitter_server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 326ac8d911..03762ce75e 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -14,7 +14,8 @@ twitter: { id: identity.id, screenName: identity.screen_name, - accessToken: oauth.accessToken + accessToken: oauth.accessToken, + accessTokenSecret: oauth.accessTokenSecret } } }, From 98c364338f12f3eeaa573210aa09e935b71fbd6a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Sat, 18 Aug 2012 01:09:16 -0700 Subject: [PATCH 28/38] Various resturcturing of the relationship between the different oauth packages --- packages/accounts-facebook/facebook_client.js | 2 +- packages/accounts-facebook/facebook_server.js | 2 +- packages/accounts-google/google_client.js | 2 +- packages/accounts-google/google_server.js | 2 +- .../accounts-oauth-helper/oauth_client.js | 10 +- .../accounts-oauth-helper/oauth_server.js | 115 +++++++++++------- .../accounts-oauth1-helper/oauth1_server.js | 81 +++++------- .../accounts-oauth2-helper/oauth2_server.js | 57 ++++----- packages/accounts-twitter/twitter_client.js | 2 +- packages/accounts-twitter/twitter_server.js | 2 +- packages/accounts-weibo/weibo_client.js | 2 +- packages/accounts-weibo/weibo_server.js | 2 +- 12 files changed, 145 insertions(+), 134 deletions(-) diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 420717d5e8..3d34aac30e 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -17,7 +17,7 @@ '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + '&display=' + display + '&scope=' + scope + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl); }; })(); diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index e7d521ca6f..35b21146eb 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -4,7 +4,7 @@ Meteor.accounts.facebook._secret = secret; }; - Meteor.accounts.oauth.registerService('facebook', {version: 2}, function(query) { + Meteor.accounts.oauth.registerService('facebook', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 8bab1862ff..431b853fe9 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -25,7 +25,7 @@ '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl); }; }) (); diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 49f6cd36ff..b13ce08748 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -4,7 +4,7 @@ Meteor.accounts.google._secret = secret; }; - Meteor.accounts.oauth.registerService('google', {version: 2}, function(query) { + Meteor.accounts.oauth.registerService('google', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken); diff --git a/packages/accounts-oauth-helper/oauth_client.js b/packages/accounts-oauth-helper/oauth_client.js index f2e1ce9654..c6a91b5608 100644 --- a/packages/accounts-oauth-helper/oauth_client.js +++ b/packages/accounts-oauth-helper/oauth_client.js @@ -3,7 +3,7 @@ // // @param state {String} The OAuth state generated by the client // @param url {String} url to page - Meteor.accounts.oauth.initiateLogin = function(state, url, options) { + Meteor.accounts.oauth.initiateLogin = function(state, url) { // XXX these dimensions worked well for facebook and google, but // it's sort of weird to have these here. Maybe an optional // argument instead? @@ -12,7 +12,7 @@ var checkPopupOpen = setInterval(function() { if (popup.closed) { clearInterval(checkPopupOpen); - tryLoginAfterPopupClosed(state, options); + tryLoginAfterPopupClosed(state); } }, 100); }; @@ -20,9 +20,9 @@ // Send an OAuth login method to the server. If the user authorized // access in the popup this should log the user in, otherwise // nothing should happen. - var tryLoginAfterPopupClosed = function(state, options) { + var tryLoginAfterPopupClosed = function(state) { Meteor.apply('login', [ - {oauth: {version: options.version, state: state}} + {oauth: {state: state}} ], {wait: true}, function(error, result) { if (error) throw error; @@ -59,4 +59,4 @@ newwindow.focus(); return newwindow; }; -})(); \ No newline at end of file +})(); diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index ef4c9ee231..84466b594e 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -1,57 +1,99 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); + Meteor.accounts.oauth._services = {}; + // Register a handler for an OAuth service. The handler will be called // when we get an incoming http request on /_oauth/{serviceName}. This // handler should use that information to fetch data about the user // logging in. // // @param name {String} e.g. "google", "facebook" + // @param version {Number} OAuth version (1 or 2) // @param handleOauthRequest {Function(query)} // - query is an object with the parameters passed in the query string // - return value is: // - {options: (options), extra: (optional extra)} (same as the // arguments to Meteor.accounts.updateOrCreateUser) // - `null` if the user declined to give permissions - Meteor.accounts.oauth.registerService = function (name, options, handleOauthRequest) { - var oauthAccounts = Meteor.accounts['oauth' + options.version]; + Meteor.accounts.oauth.registerService = function (name, version, handleOauthRequest) { + if (Meteor.accounts.oauth._services[name]) + throw new Error("Already registered the " + name + " OAuth service"); - if (oauthAccounts._services[name]) - throw new Error("Already registered the " + name + " OAuth" + options.version + " service"); - - oauthAccounts._services[name] = { + Meteor.accounts.oauth._services[name] = { + serviceName: name, + version: version, handleOauthRequest: handleOauthRequest }; }; - Meteor.accounts.oauth._setup = function(setupOptions) { - var oauthAccounts = Meteor.accounts['oauth' + setupOptions.version]; + // When we get an incoming OAuth http request we complete the oauth + // handshake, account and token setup before responding. The + // results are stored in this map which is then read when the login + // method is called. Maps state --> return value of `login` + // + // XXX we should periodically clear old entries + Meteor.accounts.oauth._loginResultForState = {}; - // Listen to calls to `login` with an oauth option set - Meteor.accounts.registerLoginHandler(function (options) { - if (!options.oauth || options.oauth.version !== setupOptions.version) - return undefined; // don't handle + // Listen to calls to `login` with an oauth option set + Meteor.accounts.registerLoginHandler(function (options) { + if (!options.oauth) + return undefined; // don't handle - var result = oauthAccounts._loginResultForState[options.oauth.state]; - if (result === undefined) // not using `!result` since can be null - // We weren't notified of the user authorizing the login. - return null; - else - return result; + var result = Meteor.accounts.oauth._loginResultForState[options.oauth.state]; + if (result === undefined) // not using `!result` since can be null + // We weren't notified of the user authorizing the login. + return null; + else + return result; + }); + + // Listen to incoming OAuth http requests + __meteor_bootstrap__.app + .use(connect.query()) + .use(function(req, res, next) { + // Need to create a Fiber since we're using synchronous http + // calls and nothing else is wrapping this in a fiber + // automatically + Fiber(function () { + middleware(req, res, next); + }).run(); }); - - // When we get an incoming OAuth http request we complete the oauth - // handshake, account and token setup before responding. The - // results are stored in this map which is then read when the login - // method is called. Maps state --> return value of `login` - // - // XXX we should periodically clear old entries - oauthAccounts._loginResultForState = {}; + var middleware = function (req, res, next) { + var serviceName = requestServiceName(req); + if (!serviceName) { + // not an oauth request. pass to next middleware. + next(); + return; + } + + var service = Meteor.accounts.oauth._services[serviceName]; + + // Skip everything if there's no service set by the oauth middleware + // XXX should we instead throw an error? + // XXX we should catch all exceptions here as we do in oauth2_server.js + if (!service) { + next(); + return; + } + + // Make sure we're configured + ensureConfigured(serviceName); + + if (service.version === 1) + Meteor.accounts.oauth1._handleRequest(service, req.query, res); + else if (service.version === 2) + Meteor.accounts.oauth2._handleRequest(service, req.query, res); + else + throw new Error("Unexpected OAuth version " + service.version); }; // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware - Meteor.accounts.oauth._requestServiceName = function (req) { + // + // @returns {String|null} e.g. "facebook", or null if this isn't an + // oauth request + var requestServiceName = function (req) { // req.url will be "/_oauth/?" var barePath = req.url.substring(0, req.url.indexOf('?')); @@ -63,14 +105,14 @@ // Any non-oauth request will continue down the default middlewares // Same goes for service that hasn't been registered if (splitPath[1] !== '_oauth') { - return; + return null; } return serviceName; }; // Make sure we're configured - Meteor.accounts.oauth._ensureConfigured = function(serviceName) { + var ensureConfigured = function(serviceName) { var service = Meteor.accounts[serviceName]; _.each(Meteor.accounts[serviceName]._requireConfigs, function(key) { @@ -83,17 +125,6 @@ throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); }; - Meteor.accounts.oauth._loadMiddleWare = function(middleware) { - __meteor_bootstrap__.app - .use(connect.query()) - .use(function(req, res, next) { - // Need to create a Fiber since we're using synchronous http - // calls and nothing else is wrapping this in a fiber - // automatically - Fiber(function () { - middleware(req, res, next); - }).run(); - }); - }; - })(); + + diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 8eb860e598..3225d85eff 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -1,24 +1,11 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); - Meteor.accounts.oauth1._services = {}; - - Meteor.accounts.oauth._setup({version: 1}); + // XXX probably need to catch exceptions here as we do in oauth2_server.js + // or put that in oauth_server.js instead // connect middleware - Meteor.accounts.oauth1._handleRequest = function (req, res, next) { - - var serviceName = Meteor.accounts.oauth._requestServiceName(req); - var service = Meteor.accounts.oauth1._services[serviceName]; - - // Skip everything if there's no service set by the oauth middleware - if (!service) { - next(); - return; - } - - // Make sure we're configured - Meteor.accounts.oauth._ensureConfigured(serviceName); + Meteor.accounts.oauth1._handleRequest = function (service, query, res) { // Make sure we prepare the login results before returning. // This way the subsequent call to the `login` method will be @@ -29,10 +16,10 @@ // If we get here with a callback url we need a request token to // start the logic process - if (req.query.callbackUrl) { + if (query.callbackUrl) { // Get a request token to start auth process - oauth.getRequestToken(req.query.callbackUrl); + oauth.getRequestToken(query.callbackUrl); var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauth.requestToken; res.writeHead(302, {'Location': redirectUrl}); @@ -40,53 +27,49 @@ // If we get here without a callback url we've just // returned from authentication via the oauth provider - + } else { // XXX Twitter's docs say to check that oauth_token is the // same as the request token received in previous step - if (!req.query.oauth_token) { - // The user didn't authorize access - return null; - } + if (query.oauth_token) { + // The user authorized access - // Get the oauth token for signing requests - oauth.getAccessToken(req.query); + // Get the oauth token for signing requests + oauth.getAccessToken(query); - // Get or create user id - var oauthResult = service.handleOauthRequest(oauth); - - if (oauthResult) { // could be null if user declined permissions + // Get or create user id + var oauthResult = service.handleOauthRequest(oauth); var userId = Meteor.accounts.updateOrCreateUser(oauthResult.options, oauthResult.extra); - + // Generate and store a login token for reconnect // XXX this could go in accounts_server.js instead var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - + // Store results to subsequent call to `login` - Meteor.accounts.oauth1._loginResultForState[req.query.state] = + Meteor.accounts.oauth._loginResultForState[query.state] = {token: loginToken, id: userId}; } - - // We support ?close and ?redirect=URL. Any other query should - // just serve a blank page - if ('close' in req.query) { // check with 'in' because we don't set a value - // Close the popup window - res.writeHead(200, {'Content-Type': 'text/html'}); - var content = - ''; - res.end(content, 'utf-8'); - } else if (req.query.redirect) { - res.writeHead(302, {'Location': req.query.redirect}); - res.end(); - } else { - res.writeHead(200, {'Content-Type': 'text/html'}); + } + + // XXX push down to oauth_server.js? + + // We support ?close and ?redirect=URL. Any other query should + // just serve a blank page + if ('close' in query) { // check with 'in' because we don't set a value + // Close the popup window + res.writeHead(200, {'Content-Type': 'text/html'}); + var content = + ''; + res.end(content, 'utf-8'); + } else if (query.redirect) { + res.writeHead(302, {'Location': query.redirect}); + res.end(); + } else { + res.writeHead(200, {'Content-Type': 'text/html'}); res.end('', 'utf-8'); - } } }; - Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth1._handleRequest); - })(); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 3e5254c69a..ce45da67dd 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -1,59 +1,58 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); - Meteor.accounts.oauth2._services = {}; - - Meteor.accounts.oauth._setup({version: 2}); - // connect middleware - Meteor.accounts.oauth2._handleRequest = function (req, res, next) { - - var serviceName = Meteor.accounts.oauth._requestServiceName(req); - var service = Meteor.accounts.oauth2._services[serviceName]; - - // Skip everything if there's no service set by the oauth middleware - if (!service) { - next(); - return; - } - - // Make sure we're configured - Meteor.accounts.oauth._ensureConfigured(serviceName); - - if (req.query.error) { + Meteor.accounts.oauth2._handleRequest = function (service, query, res) { + if (query.error) { // The user didn't authorize access - return null; + return; } // Make sure we prepare the login results before returning. // This way the subsequent call to the `login` method will be // immediate. - // Get or create user id - var oauthResult = service.handleOauthRequest(req.query); + try { + // Get or create user id + var oauthResult = service.handleOauthRequest(query); - if (oauthResult) { // could be null if user declined permissions - var userId = Meteor.accounts.updateOrCreateUser(oauthResult.options, oauthResult.extra); + var userId = Meteor.accounts.updateOrCreateUser( + oauthResult.options, oauthResult.extra); // Generate and store a login token for reconnect // XXX this could go in accounts_server.js instead var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); // Store results to subsequent call to `login` - Meteor.accounts.oauth2._loginResultForState[req.query.state] = + Meteor.accounts.oauth._loginResultForState[query.state] = {token: loginToken, id: userId}; + } catch (err) { + // if we got thrown an error, save it off, it will get passed to + // the approporiate login call (if any) and reported there. + // + // The other option would be to display it in the popup tab that + // is still open at this point, ignoring the 'close' or 'redirect' + // we were passed. But then the developer wouldn't be able to + // style the error or react to it in any way. + if (query.state && err instanceof Error) + Meteor.accounts.oauth._loginResultForState[query.state] = err; + + // also log to the server console, so the developer sees it. + Meteor._debug("Exception in oauth2 handler", err); } + // XXX push down to oauth_server.js? + // We support ?close and ?redirect=URL. Any other query should // just serve a blank page - if ('close' in req.query) { // check with 'in' because we don't set a value + if ('close' in query) { // check with 'in' because we don't set a value // Close the popup window res.writeHead(200, {'Content-Type': 'text/html'}); var content = ''; res.end(content, 'utf-8'); - } else if (req.query.redirect) { - res.writeHead(302, {'Location': req.query.redirect}); + } else if (query.redirect) { + res.writeHead(302, {'Location': query.redirect}); res.end(); } else { res.writeHead(200, {'Content-Type': 'text/html'}); @@ -61,6 +60,4 @@ } }; - Meteor.accounts.oauth._loadMiddleWare(Meteor.accounts.oauth2._handleRequest); - })(); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 38a33011e1..d93aa01cc9 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -7,7 +7,7 @@ var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) - Meteor.accounts.oauth.initiateLogin(state, url, { version: 1 }); + Meteor.accounts.oauth.initiateLogin(state, url); }; })(); diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 03762ce75e..2cdab4baa7 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -4,7 +4,7 @@ Meteor.accounts.twitter._secret = secret; }; - Meteor.accounts.oauth.registerService('twitter', {version: 1}, function(oauth) { + Meteor.accounts.oauth.registerService('twitter', 1, function(oauth) { var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 7855f9be1c..9371dc08cd 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -12,7 +12,7 @@ '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + '&state=' + state; - Meteor.accounts.oauth.initiateLogin(state, loginUrl, { version: 2 }); + Meteor.accounts.oauth.initiateLogin(state, loginUrl); }; }) (); diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 034ade041d..9a4797af9d 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -4,7 +4,7 @@ Meteor.accounts.weibo._secret = secret; }; - Meteor.accounts.oauth.registerService('weibo', {version: 2}, function(query) { + Meteor.accounts.oauth.registerService('weibo', 2, function(query) { var accessToken = getAccessToken(query); var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10)); From 9844a169e7c7e69ef503c8bcc1f66ebec05de6bb Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Sat, 18 Aug 2012 01:58:13 -0700 Subject: [PATCH 29/38] Unbreak failing OAuth2 tests --- .../accounts-oauth-helper/oauth_server.js | 8 +++-- .../accounts-oauth1-helper/oauth1_tests.js | 14 ++++----- .../accounts-oauth2-helper/oauth2_tests.js | 30 +++++++++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 84466b594e..a4e1a6d38a 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -44,6 +44,10 @@ if (result === undefined) // not using `!result` since can be null // We weren't notified of the user authorizing the login. return null; + else if (result instanceof Error) + // We tried to login, but there was a fatal error. Report it back + // to the user. + throw result; else return result; }); @@ -56,11 +60,11 @@ // calls and nothing else is wrapping this in a fiber // automatically Fiber(function () { - middleware(req, res, next); + Meteor.accounts.oauth._middleware(req, res, next); }).run(); }); - var middleware = function (req, res, next) { + Meteor.accounts.oauth._middleware = function (req, res, next) { var serviceName = requestServiceName(req); if (!serviceName) { // not an oauth request. pass to next middleware. diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 0a5e5774a7..3a7ecc4b2d 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -1,13 +1,13 @@ -Tinytest.add("oauth2 - loginResultForState is stored", function (test) { +Tinytest.add("oauth1 - loginResultForState is stored", function (test) { var http = __meteor_bootstrap__.require('http'); var email = Meteor.uuid() + "@example.com"; Meteor.accounts._loginTokens.remove({}); - Meteor.accounts.oauth1._loginResultForState = {}; - Meteor.accounts.oauth1._services = {}; + Meteor.accounts.oauth._loginResultForState = {}; + Meteor.accounts.oauth._services = {}; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", {version: 1}, function (query) { + Meteor.accounts.oauth.registerService("foobook", 1, function (query) { return { options: { email: email, @@ -20,7 +20,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { var req = {method: "POST", url: "/_oauth/foobook?close", query: {state: "STATE"}}; - Meteor.accounts.oauth1._handleRequest(req, new http.ServerResponse(req)); + Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is created var user = Meteor.users.findOne({emails: email}); @@ -33,7 +33,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // and that the login result for that user is prepared test.equal( - Meteor.accounts.oauth1._loginResultForState['STATE'].id, user._id); + Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id); test.equal( - Meteor.accounts.oauth1._loginResultForState['STATE'].token, token._id); + Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id); }); diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index b1b290c2c1..c2d5daeda3 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -6,11 +6,16 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // XXX XXX test isolation fail! Avital: but actually -- why would // we run server tests more than once? or even more so in parallel? Meteor.accounts._loginTokens.remove({}); - Meteor.accounts.oauth2._loginResultForState = {}; - Meteor.accounts.oauth2._services = {}; + Meteor.accounts.oauth._loginResultForState = {}; + Meteor.accounts.oauth._services = {}; + + // XXX this shouldn't be necessary + Meteor.accounts.foobook = {}; + Meteor.accounts.foobook._requireConfigs = []; + Meteor.accounts.foobook._secret = 'XXX'; // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", {version: 2}, function (query) { + Meteor.accounts.oauth.registerService("foobook", 2, function (query) { return { options: { email: email, @@ -23,7 +28,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { var req = {method: "POST", url: "/_oauth/foobook?close", query: {state: "STATE"}}; - Meteor.accounts.oauth2._handleRequest(req, new http.ServerResponse(req)); + Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is created var user = Meteor.users.findOne({"emails.email": email}); @@ -37,9 +42,9 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { // and that the login result for that user is prepared test.equal( - Meteor.accounts.oauth2._loginResultForState['STATE'].id, user._id); + Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id); test.equal( - Meteor.accounts.oauth2._loginResultForState['STATE'].token, token._id); + Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id); }); @@ -48,8 +53,13 @@ Tinytest.add("oauth2 - error in user creation", function (test) { var email = Meteor.uuid() + "@example.com"; var state = Meteor.uuid(); + // XXX this shouldn't be necessary + Meteor.accounts.failbook = {}; + Meteor.accounts.failbook._requireConfigs = []; + Meteor.accounts.failbook._secret = 'XXX'; + // register a failing login service - Meteor.accounts.oauth2.registerService("failbook", function (query) { + Meteor.accounts.oauth.registerService("failbook", 2, function (query) { return { options: { email: email, @@ -72,14 +82,14 @@ Tinytest.add("oauth2 - error in user creation", function (test) { var req = {method: "POST", url: "/_oauth/failbook?close", query: {state: state}}; - Meteor.accounts.oauth2._handleRequest(req, new http.ServerResponse(req)); + Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is not created var user = Meteor.users.findOne({"emails.email": email}); test.equal(user, undefined); // verify an error is stored in login state - test.equal(Meteor.accounts.oauth2._loginResultForState[state].error, 403); + test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403); // verify error is handed back to login method. test.throws(function () { @@ -87,3 +97,5 @@ Tinytest.add("oauth2 - error in user creation", function (test) { }); }); + + From 8fa9ff0114b5d975ba3ab3513b31f9eb221d84ec Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Sat, 18 Aug 2012 13:38:37 -0700 Subject: [PATCH 30/38] Clarify comment --- packages/accounts-oauth2-helper/oauth2_tests.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index c2d5daeda3..d13328f4ba 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -9,7 +9,8 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; - // XXX this shouldn't be necessary + // XXX can we make this unnecessary? Not totally sold on _requireConfigs + // yet, but maybe I'm just being overly delicate. Meteor.accounts.foobook = {}; Meteor.accounts.foobook._requireConfigs = []; Meteor.accounts.foobook._secret = 'XXX'; From 4bb3072361f0deddcf2668ada5b9511ca861f765 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 21 Aug 2012 03:35:43 -0400 Subject: [PATCH 31/38] Handle oauth 1 & 2 errors in common spot --- .../accounts-oauth-helper/oauth_server.js | 28 +++++++++++---- .../accounts-oauth1-helper/oauth1_server.js | 3 +- .../accounts-oauth2-helper/oauth2_server.js | 35 ++++++------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index a4e1a6d38a..956112f5df 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -76,7 +76,6 @@ // Skip everything if there's no service set by the oauth middleware // XXX should we instead throw an error? - // XXX we should catch all exceptions here as we do in oauth2_server.js if (!service) { next(); return; @@ -85,12 +84,27 @@ // Make sure we're configured ensureConfigured(serviceName); - if (service.version === 1) - Meteor.accounts.oauth1._handleRequest(service, req.query, res); - else if (service.version === 2) - Meteor.accounts.oauth2._handleRequest(service, req.query, res); - else - throw new Error("Unexpected OAuth version " + service.version); + try { + if (service.version === 1) + Meteor.accounts.oauth1._handleRequest(service, req.query, res); + else if (service.version === 2) + Meteor.accounts.oauth2._handleRequest(service, req.query, res); + else + throw new Error("Unexpected OAuth version " + service.version); + } catch (err) { + // if we got thrown an error, save it off, it will get passed to + // the approporiate login call (if any) and reported there. + // + // The other option would be to display it in the popup tab that + // is still open at this point, ignoring the 'close' or 'redirect' + // we were passed. But then the developer wouldn't be able to + // style the error or react to it in any way. + if (req.query.state && err instanceof Error) + Meteor.accounts.oauth._loginResultForState[req.query.state] = err; + + // also log to the server console, so the developer sees it. + Meteor._debug("Exception in oauth" + service.version + " handler", err); + } }; // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 3225d85eff..5b6f71bb77 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -41,7 +41,8 @@ // Get or create user id var oauthResult = service.handleOauthRequest(oauth); - var userId = Meteor.accounts.updateOrCreateUser(oauthResult.options, oauthResult.extra); + var userId = Meteor.accounts.updateOrCreateUser( + oauthResult.options, oauthResult.extra); // Generate and store a login token for reconnect // XXX this could go in accounts_server.js instead diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index ce45da67dd..017a2540fa 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -12,34 +12,19 @@ // This way the subsequent call to the `login` method will be // immediate. - try { - // Get or create user id - var oauthResult = service.handleOauthRequest(query); + // Get or create user id + var oauthResult = service.handleOauthRequest(query); - var userId = Meteor.accounts.updateOrCreateUser( - oauthResult.options, oauthResult.extra); + var userId = Meteor.accounts.updateOrCreateUser( + oauthResult.options, oauthResult.extra); - // Generate and store a login token for reconnect - // XXX this could go in accounts_server.js instead - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + // Generate and store a login token for reconnect + // XXX this could go in accounts_server.js instead + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - // Store results to subsequent call to `login` - Meteor.accounts.oauth._loginResultForState[query.state] = - {token: loginToken, id: userId}; - } catch (err) { - // if we got thrown an error, save it off, it will get passed to - // the approporiate login call (if any) and reported there. - // - // The other option would be to display it in the popup tab that - // is still open at this point, ignoring the 'close' or 'redirect' - // we were passed. But then the developer wouldn't be able to - // style the error or react to it in any way. - if (query.state && err instanceof Error) - Meteor.accounts.oauth._loginResultForState[query.state] = err; - - // also log to the server console, so the developer sees it. - Meteor._debug("Exception in oauth2 handler", err); - } + // Store results to subsequent call to `login` + Meteor.accounts.oauth._loginResultForState[query.state] = + {token: loginToken, id: userId}; // XXX push down to oauth_server.js? From 3be22008f08ae470e49a5d4f1c2c09b98e82ef95 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 21 Aug 2012 03:53:08 -0400 Subject: [PATCH 32/38] Removed XXX comment similar to comment address by previous commit --- packages/accounts-oauth1-helper/oauth1_server.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 5b6f71bb77..c63b82bde9 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -1,9 +1,6 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); - // XXX probably need to catch exceptions here as we do in oauth2_server.js - // or put that in oauth_server.js instead - // connect middleware Meteor.accounts.oauth1._handleRequest = function (service, query, res) { From 8297e395108d9d8ff33039efec8fce23990c9ec4 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Tue, 21 Aug 2012 04:03:24 -0400 Subject: [PATCH 33/38] Move rendering oauth results to common spot --- .../accounts-oauth-helper/oauth_server.js | 18 +++++++++++++++++ .../accounts-oauth1-helper/oauth1_server.js | 20 +++---------------- .../accounts-oauth2-helper/oauth2_server.js | 20 +++---------------- 3 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 956112f5df..de59b38411 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -143,6 +143,24 @@ throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); }; + Meteor.accounts.oauth._renderOauthResults = function(res, query) { + // We support ?close and ?redirect=URL. Any other query should + // just serve a blank page + if ('close' in query) { // check with 'in' because we don't set a value + // Close the popup window + res.writeHead(200, {'Content-Type': 'text/html'}); + var content = + ''; + res.end(content, 'utf-8'); + } else if (query.redirect) { + res.writeHead(302, {'Location': query.redirect}); + res.end(); + } else { + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end('', 'utf-8'); + } + }; + })(); diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index c63b82bde9..4249d2a6b9 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -51,23 +51,9 @@ } } - // XXX push down to oauth_server.js? - - // We support ?close and ?redirect=URL. Any other query should - // just serve a blank page - if ('close' in query) { // check with 'in' because we don't set a value - // Close the popup window - res.writeHead(200, {'Content-Type': 'text/html'}); - var content = - ''; - res.end(content, 'utf-8'); - } else if (query.redirect) { - res.writeHead(302, {'Location': query.redirect}); - res.end(); - } else { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end('', 'utf-8'); - } + // Either close the window, redirect, or render nothing + // if all else fails + Meteor.accounts.oauth._renderOauthResults(res, query); }; })(); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 017a2540fa..41919e92c0 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -26,23 +26,9 @@ Meteor.accounts.oauth._loginResultForState[query.state] = {token: loginToken, id: userId}; - // XXX push down to oauth_server.js? - - // We support ?close and ?redirect=URL. Any other query should - // just serve a blank page - if ('close' in query) { // check with 'in' because we don't set a value - // Close the popup window - res.writeHead(200, {'Content-Type': 'text/html'}); - var content = - ''; - res.end(content, 'utf-8'); - } else if (query.redirect) { - res.writeHead(302, {'Location': query.redirect}); - res.end(); - } else { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end('', 'utf-8'); - } + // Either close the window, redirect, or render nothing + // if all else fails + Meteor.accounts.oauth._renderOauthResults(res, query); }; })(); From 0405b2e2e10d09b0b8baa4e7207dfd0a52c2fb5a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Tue, 21 Aug 2012 16:27:41 -0700 Subject: [PATCH 34/38] Squashed merge of Mike Bannister's code review changes --- .../accounts-oauth-helper/oauth_server.js | 7 ++----- packages/accounts-oauth-helper/package.js | 6 ------ .../accounts-oauth1-helper/oauth1_server.js | 20 +++++++++++++------ packages/accounts-twitter/twitter_client.js | 5 ++++- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index de59b38411..37fa4c6487 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -75,11 +75,8 @@ var service = Meteor.accounts.oauth._services[serviceName]; // Skip everything if there's no service set by the oauth middleware - // XXX should we instead throw an error? - if (!service) { - next(); - return; - } + if (!service) + throw new Error("Unexpected OAuth service " + serviceName); // Make sure we're configured ensureConfigured(serviceName); diff --git a/packages/accounts-oauth-helper/package.js b/packages/accounts-oauth-helper/package.js index 532b80fb16..c773ae24d6 100644 --- a/packages/accounts-oauth-helper/package.js +++ b/packages/accounts-oauth-helper/package.js @@ -10,9 +10,3 @@ Package.on_use(function (api) { api.add_files('oauth_client.js', 'client'); api.add_files('oauth_server.js', 'server'); }); - -Package.on_test(function (api) { - // XXX Fix these! - // api.use('accounts-oauth-helper', 'server'); - // api.add_files("oauth_tests.js", 'server'); -}); diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 4249d2a6b9..84be5a4ec2 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -1,6 +1,9 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); + // A place to store request tokens pending verification + Meteor.accounts.oauth1._requestTokens = {}; + // connect middleware Meteor.accounts.oauth1._handleRequest = function (service, query, res) { @@ -8,7 +11,7 @@ // This way the subsequent call to the `login` method will be // immediate. - var config = Meteor.accounts[serviceName]; + var config = Meteor.accounts[service.serviceName]; var oauth = new OAuth1(config); // If we get here with a callback url we need a request token to @@ -18,6 +21,9 @@ // Get a request token to start auth process oauth.getRequestToken(query.callbackUrl); + // Keep track of request token so we can verify it on the next step + Meteor.accounts.oauth1._requestTokens[query.state] = oauth.requestToken; + var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauth.requestToken; res.writeHead(302, {'Location': redirectUrl}); res.end(); @@ -27,13 +33,15 @@ } else { - // XXX Twitter's docs say to check that oauth_token is the - // same as the request token received in previous step + // Get the user's request token so we can verify it and clear it + var requestToken = Meteor.accounts.oauth1._requestTokens[query.state]; + delete Meteor.accounts.oauth1._requestTokens[query.state]; - if (query.oauth_token) { - // The user authorized access + // Verify user authorized access and the oauth_token matches + // the requestToken from previous step + if (query.oauth_token && query.oauth_token === requestToken) { - // Get the oauth token for signing requests + // Get the access token for signing requests oauth.getAccessToken(query); // Get or create user id diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index d93aa01cc9..8ecdafd6e2 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -4,8 +4,11 @@ throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first"); var state = Meteor.uuid(); + // We need to keep state across the next two 'steps' so we're adding + // a state parameter to the url and the callback url that we'll be returned + // to by oauth provider var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; - var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) + var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) + '&state=' + state Meteor.accounts.oauth.initiateLogin(state, url); }; From 5d6230fb59977a0adfc13c1695fbc9a735051bd6 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Wed, 22 Aug 2012 00:15:57 -0400 Subject: [PATCH 35/38] Removed automagic _ prefix in ensureConfigured --- packages/accounts-facebook/facebook_common.js | 2 +- packages/accounts-google/google_common.js | 2 +- packages/accounts-oauth-helper/oauth_server.js | 3 +-- packages/accounts-twitter/twitter_common.js | 2 +- packages/accounts-weibo/weibo_common.js | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js index e1697e3f5a..d3ba6e0771 100644 --- a/packages/accounts-facebook/facebook_common.js +++ b/packages/accounts-facebook/facebook_common.js @@ -1,6 +1,6 @@ if (!Meteor.accounts.facebook) { Meteor.accounts.facebook = {}; - Meteor.accounts.facebook._requireConfigs = ['appId', 'appUrl']; + Meteor.accounts.facebook._requireConfigs = ['_appId', '_appUrl']; } Meteor.accounts.facebook.config = function(appId, appUrl, options) { diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js index f53669a543..e5071871bc 100644 --- a/packages/accounts-google/google_common.js +++ b/packages/accounts-google/google_common.js @@ -1,6 +1,6 @@ if (!Meteor.accounts.google) { Meteor.accounts.google = {}; - Meteor.accounts.google._requireConfigs = ['clientId', 'appUrl']; + Meteor.accounts.google._requireConfigs = ['_clientId', '_appUrl']; } Meteor.accounts.google.config = function(clientId, appUrl, options) { diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 37fa4c6487..49c814dc68 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -131,8 +131,7 @@ var service = Meteor.accounts[serviceName]; _.each(Meteor.accounts[serviceName]._requireConfigs, function(key) { - var configKey = '_' + key; - if (!service[configKey]) + if (!service[key]) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); }); diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index e38e8124fa..812100b696 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,6 +1,6 @@ if (!Meteor.accounts.twitter) { Meteor.accounts.twitter = {}; - Meteor.accounts.twitter._requireConfigs = ['appId', 'appUrl']; + Meteor.accounts.twitter._requireConfigs = ['_appId', '_appUrl']; } Meteor.accounts.twitter.config = function(appId, appUrl, options) { diff --git a/packages/accounts-weibo/weibo_common.js b/packages/accounts-weibo/weibo_common.js index 6c21d9bffc..9f9d3f0692 100644 --- a/packages/accounts-weibo/weibo_common.js +++ b/packages/accounts-weibo/weibo_common.js @@ -1,6 +1,6 @@ if (!Meteor.accounts.weibo) { Meteor.accounts.weibo = {}; - Meteor.accounts.weibo._requireConfigs = ['clientId', 'appUrl']; + Meteor.accounts.weibo._requireConfigs = ['_clientId', '_appUrl']; } Meteor.accounts.weibo.config = function(clientId, appUrl) { From b6ec3d59d472eb821d63942e1267bda0cbed0045 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Wed, 22 Aug 2012 01:13:16 -0400 Subject: [PATCH 36/38] Tests for oauth1 --- .../accounts-oauth1-helper/oauth1_tests.js | 127 ++++++++++++++++-- packages/accounts-oauth1-helper/package.js | 5 +- 2 files changed, 118 insertions(+), 14 deletions(-) diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 3a7ecc4b2d..1514d8ab1f 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -1,31 +1,64 @@ + Tinytest.add("oauth1 - loginResultForState is stored", function (test) { var http = __meteor_bootstrap__.require('http'); - var email = Meteor.uuid() + "@example.com"; + var twitterfooId = Meteor.uuid(); + var twitterfooName = 'nickname' + Meteor.uuid(); + var twitterfooAccessToken = Meteor.uuid(); + var twitterfooAccessTokenSecret = Meteor.uuid(); + OAuth1.prototype.getRequestToken = function() {}; + OAuth1.prototype.getAccessToken = function() { + this.accessToken = twitterfooAccessToken; + this.accessTokenSecret = twitterfooAccessTokenSecret; + }; + + // XXX XXX test isolation fail! Avital: but actually -- why would + // we run server tests more than once? or even more so in parallel? Meteor.accounts._loginTokens.remove({}); Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; - // register a fake login service - foobook - Meteor.accounts.oauth.registerService("foobook", 1, function (query) { + // XXX can we make this unnecessary? Not totally sold on _requireConfigs + // yet, but maybe I'm just being overly delicate. + Meteor.accounts.twitterfoo = {}; + Meteor.accounts.twitterfoo._requireConfigs = []; + Meteor.accounts.twitterfoo._secret = 'XXX'; + + // register a fake login service - twitterfoo + Meteor.accounts.oauth.registerService("twitterfoo", 1, function (query) { return { options: { - email: email, - services: {foobook: {id: 1}} + services: { + twitter: { + id: twitterfooId, + screenName: twitterfooName, + accessToken: twitterfooAccessToken, + accessTokenSecret: twitterfooAccessTokenSecret + } + } } }; }); - // simulate logging in using foobook - var req = {method: "POST", - url: "/_oauth/foobook?close", - query: {state: "STATE"}}; + // simulate logging in using twitterfoo + Meteor.accounts.oauth1._requestTokens['STATE'] = twitterfooAccessToken; + + var req = { + method: "POST", + url: "/_oauth/twitterfoo?close", + query: { + state: "STATE", + oauth_token: twitterfooAccessToken + } + }; + Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); // verify that a user is created - var user = Meteor.users.findOne({emails: email}); + var user = Meteor.users.findOne({"services.twitter.screenName": twitterfooName}); test.notEqual(user, undefined); - test.equal(user.services.foobook.id, 1); + test.equal(user.services.twitter.accessToken, twitterfooAccessToken); + test.equal(user.services.twitter.accessTokenSecret, twitterfooAccessTokenSecret); // and that that user has a login token var token = Meteor.accounts._loginTokens.findOne({userId: user._id}); @@ -37,3 +70,75 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { test.equal( Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id); }); + + +Tinytest.add("oauth1 - error in user creation", function (test) { + var http = __meteor_bootstrap__.require('http'); + var state = Meteor.uuid(); + var twitterfailId = Meteor.uuid(); + var twitterfailName = 'nickname' + Meteor.uuid(); + var twitterfailAccessToken = Meteor.uuid(); + var twitterfailAccessTokenSecret = Meteor.uuid(); + + // XXX can we make this unnecessary? Not totally sold on _requireConfigs + // yet, but maybe I'm just being overly delicate. + Meteor.accounts.twitterfail = {}; + Meteor.accounts.twitterfail._requireConfigs = []; + Meteor.accounts.twitterfail._secret = 'XXX'; + + // Wire up access token so that verification passes + Meteor.accounts.oauth1._requestTokens[state] = twitterfailAccessToken; + + // register a failing login service + Meteor.accounts.oauth.registerService("twitterfail", 1, function (query) { + return { + options: { + services: { + twitter: { + id: twitterfailId, + screenName: twitterfailName, + accessToken: twitterfailAccessToken, + accessTokenSecret: twitterfailAccessTokenSecret + } + } + }, + extra: { + invalid: true + } + }; + }); + + // a way to fail new users. duplicated from passwords_tests, but + // shouldn't hurt. + Meteor.accounts.validateNewUser(function (user) { + return !user.invalid; + }); + + // simulate logging in with failure + Meteor._suppress_log(1); + var req = { + method: "POST", + url: "/_oauth/twitterfail?close", + query: { + state: state, + oauth_token: twitterfailAccessToken + } + }; + + Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req)); + + // verify that a user is not created + var user = Meteor.users.findOne({"services.twitter.screenName": twitterfailName}); + test.equal(user, undefined); + + // verify an error is stored in login state + test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403); + + // verify error is handed back to login method. + test.throws(function () { + Meteor.apply('login', [{oauth: {version: 1, state: state}}]); + }); + +}); + + diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js index a42b9b1805..de76baa5d3 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/accounts-oauth1-helper/package.js @@ -12,7 +12,6 @@ Package.on_use(function (api) { }); Package.on_test(function (api) { - // XXX Fix these! - // api.use('accounts-oauth1-helper', 'server'); - // api.add_files("oauth1_tests.js", 'server'); + api.use('accounts-oauth1-helper', 'server'); + api.add_files("oauth1_tests.js", 'server'); }); From a2eda047feab6d156b708848234295eb356bd680 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 22 Aug 2012 22:02:40 -0700 Subject: [PATCH 37/38] A big refactor and cleanup on oauth{1,2} and twitter login --- examples/todos/.meteor/packages | 1 + .../accounts-oauth-helper/oauth_server.js | 85 +++++++++++-------- .../{oauth1.js => oauth1_binding.js} | 66 ++++++++------ .../accounts-oauth1-helper/oauth1_server.js | 33 ++++--- .../accounts-oauth1-helper/oauth1_tests.js | 10 +-- packages/accounts-oauth1-helper/package.js | 2 +- .../accounts-oauth2-helper/oauth2_server.js | 39 ++++----- .../accounts-oauth2-helper/oauth2_tests.js | 3 - packages/accounts-twitter/twitter_client.js | 11 ++- packages/accounts-twitter/twitter_common.js | 22 ++--- packages/accounts-twitter/twitter_server.js | 13 ++- packages/accounts-ui/login_buttons.html | 2 +- 12 files changed, 155 insertions(+), 132 deletions(-) rename packages/accounts-oauth1-helper/{oauth1.js => oauth1_binding.js} (51%) diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages index c71f4c1a54..0aff14a890 100644 --- a/examples/todos/.meteor/packages +++ b/examples/todos/.meteor/packages @@ -12,3 +12,4 @@ accounts-weibo accounts-google accounts-facebook accounts-passwords +accounts-twitter diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 49c814dc68..7f2f3aa9aa 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -10,8 +10,9 @@ // // @param name {String} e.g. "google", "facebook" // @param version {Number} OAuth version (1 or 2) - // @param handleOauthRequest {Function(query)} - // - query is an object with the parameters passed in the query string + // @param handleOauthRequest {Function(oauthBinding|query)} + // - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider + // - (For OAuth2 only) query {Object} parameters passed in query string // - return value is: // - {options: (options), extra: (optional extra)} (same as the // arguments to Meteor.accounts.updateOrCreateUser) @@ -65,23 +66,25 @@ }); Meteor.accounts.oauth._middleware = function (req, res, next) { - var serviceName = requestServiceName(req); - if (!serviceName) { - // not an oauth request. pass to next middleware. - next(); - return; - } - - var service = Meteor.accounts.oauth._services[serviceName]; - - // Skip everything if there's no service set by the oauth middleware - if (!service) - throw new Error("Unexpected OAuth service " + serviceName); - - // Make sure we're configured - ensureConfigured(serviceName); - + // Make sure to catch any exceptions because otherwise we'd crash + // the runner try { + var serviceName = oauthServiceName(req); + if (!serviceName) { + // not an oauth request. pass to next middleware. + next(); + return; + } + + var service = Meteor.accounts.oauth._services[serviceName]; + + // Skip everything if there's no service set by the oauth middleware + if (!service) + throw new Error("Unexpected OAuth service " + serviceName); + + // Make sure we're configured + ensureConfigured(serviceName); + if (service.version === 1) Meteor.accounts.oauth1._handleRequest(service, req.query, res); else if (service.version === 2) @@ -100,29 +103,32 @@ Meteor.accounts.oauth._loginResultForState[req.query.state] = err; // also log to the server console, so the developer sees it. - Meteor._debug("Exception in oauth" + service.version + " handler", err); + Meteor._debug("Exception in oauth request handler", err); + + // close the popup. because nobody likes them just hanging + // there. when someone sees this multiple times they might + // think to check server logs (we hope?) + closePopup(res); } }; - // Handle _oauth paths, gets a bunch of stuff ready for the oauth implementation middleware + // Handle /_oauth/* paths and extract the service name // // @returns {String|null} e.g. "facebook", or null if this isn't an // oauth request - var requestServiceName = function (req) { + var oauthServiceName = function (req) { // req.url will be "/_oauth/?" var barePath = req.url.substring(0, req.url.indexOf('?')); var splitPath = barePath.split('/'); + // Any non-oauth request will continue down the default + // middlewares. + if (splitPath[1] !== '_oauth') + return null; + // Find service based on url var serviceName = splitPath[2]; - - // Any non-oauth request will continue down the default middlewares - // Same goes for service that hasn't been registered - if (splitPath[1] !== '_oauth') { - return null; - } - return serviceName; }; @@ -130,24 +136,22 @@ var ensureConfigured = function(serviceName) { var service = Meteor.accounts[serviceName]; - _.each(Meteor.accounts[serviceName]._requireConfigs, function(key) { + _.each(service._requireConfigs, function(key) { if (!service[key]) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".config first"); + throw new Meteor.accounts.ConfigError( + "Need to call Meteor.accounts." + serviceName + ".config first"); }); - if (!service._secret) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts." + serviceName + ".setSecret first"); + if (Meteor.is_server && !service._secret) + throw new Meteor.accounts.ConfigError( + "Need to call Meteor.accounts." + serviceName + ".setSecret first"); }; Meteor.accounts.oauth._renderOauthResults = function(res, query) { // We support ?close and ?redirect=URL. Any other query should // just serve a blank page if ('close' in query) { // check with 'in' because we don't set a value - // Close the popup window - res.writeHead(200, {'Content-Type': 'text/html'}); - var content = - ''; - res.end(content, 'utf-8'); + closePopup(res); } else if (query.redirect) { res.writeHead(302, {'Location': query.redirect}); res.end(); @@ -157,6 +161,13 @@ } }; + var closePopup = function(res) { + res.writeHead(200, {'Content-Type': 'text/html'}); + var content = + ''; + res.end(content, 'utf-8'); + }; + })(); diff --git a/packages/accounts-oauth1-helper/oauth1.js b/packages/accounts-oauth1-helper/oauth1_binding.js similarity index 51% rename from packages/accounts-oauth1-helper/oauth1.js rename to packages/accounts-oauth1-helper/oauth1_binding.js index 324eef621e..3cfb3d47a1 100644 --- a/packages/accounts-oauth1-helper/oauth1.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -1,23 +1,38 @@ var crypto = __meteor_bootstrap__.require("crypto"); var querystring = __meteor_bootstrap__.require("querystring"); -OAuth1 = function(config) { - _.extend(this, config); +// An OAuth1 wrapper around http calls which helps get tokens and +// takes care of HTTP headers +// +// @param consumerKey {String} As supplied by the OAuth1 provider +// @param consumerSecret {String} As supplied by the OAuth1 provider +// @param urls {Object} +// - requestToken (String): url +// - authorize (String): url +// - accessToken (String): url +// - authenticate (String): url +OAuth1Binding = function(consumerKey, consumerSecret, urls) { + this._consumerKey = consumerKey; + this._secret = consumerSecret; + this._urls = urls; }; -OAuth1.prototype.getRequestToken = function(callbackUrl) { +OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { var headers = this._buildHeader({ oauth_callback: callbackUrl }); - var response = this._call('post', this._urls.requestToken, headers); + var response = this._call('POST', this._urls.requestToken, headers); var tokens = querystring.parse(response.content); + // XXX should we also store oauth_token_secret here? + if (!tokens.oauth_callback_confirmed) + throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens); this.requestToken = tokens.oauth_token; }; -OAuth1.prototype.getAccessToken = function(query) { +OAuth1Binding.prototype.prepareAccessToken = function(query) { var headers = this._buildHeader({ oauth_token: query.oauth_token }); @@ -26,30 +41,29 @@ OAuth1.prototype.getAccessToken = function(query) { oauth_verifier: query.oauth_verifier }; - var response = this._call('post', this._urls.accessToken, headers, params); + var response = this._call('POST', this._urls.accessToken, headers, params); var tokens = querystring.parse(response.content); this.accessToken = tokens.oauth_token; this.accessTokenSecret = tokens.oauth_token_secret; }; -OAuth1.prototype.call = function(method, url) { +OAuth1Binding.prototype.call = function(method, url) { var headers = this._buildHeader({ oauth_token: this.accessToken }); - - var response = this._call(method, url, headers); + var response = this._call(method, url, headers); return response.data; }; -OAuth1.prototype.get = function(url) { - return this.call('get', url); +OAuth1Binding.prototype.get = function(url) { + return this.call('GET', url); }; -OAuth1.prototype._buildHeader = function(headers) { +OAuth1Binding.prototype._buildHeader = function(headers) { return _.extend({ - oauth_consumer_key: this._appId, + oauth_consumer_key: this._consumerKey, oauth_nonce: Meteor.uuid().replace(/\W/g, ''), oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), @@ -57,7 +71,7 @@ OAuth1.prototype._buildHeader = function(headers) { }, headers); }; -OAuth1.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) { +OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, accessTokenSecret) { var headers = this._encodeHeader(rawHeaders); @@ -72,43 +86,45 @@ OAuth1.prototype._getSignature = function(method, url, rawHeaders, oauthSecret) ].join('&'); var signingKey = encodeURIComponent(this._secret) + '&'; - if (oauthSecret) - signingKey += encodeURIComponent(oauthSecret); + if (accessTokenSecret) + signingKey += encodeURIComponent(accessTokenSecret); return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64'); }; -OAuth1.prototype._call = function(method, url, headers, params) { - +OAuth1Binding.prototype._call = function(method, url, headers, params) { + // Get the signature - headers.oauth_signature = this._getSignature(method.toUpperCase(), url, headers, this.accessTokenSecret); + headers.oauth_signature = this._getSignature(method, url, headers, this.accessTokenSecret); // Make a authorization string according to oauth1 spec var authString = this._getAuthHeaderString(headers); // Make signed request - var response = Meteor.http[method.toLowerCase()](url, { + var response = Meteor.http.call(method, url, { params: params, headers: { Authorization: authString } }); - if (response.error) + if (response.error) { + Meteor._debug('Error sending OAuth1 HTTP call', method, url, params, authString); throw response.error; - + } + return response; }; -OAuth1.prototype._encodeHeader = function(header) { +OAuth1Binding.prototype._encodeHeader = function(header) { return _.reduce(header, function(memo, val, key) { memo[encodeURIComponent(key)] = encodeURIComponent(val); return memo; }, {}); }; -OAuth1.prototype._getAuthHeaderString = function(headers) { +OAuth1Binding.prototype._getAuthHeaderString = function(headers) { return 'OAuth ' + _.map(headers, function(val, key) { - return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; + return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"'; }).sort().join(', '); }; diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 84be5a4ec2..3203f08955 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -7,45 +7,44 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (service, query, res) { - // Make sure we prepare the login results before returning. - // This way the subsequent call to the `login` method will be - // immediate. - var config = Meteor.accounts[service.serviceName]; - var oauth = new OAuth1(config); + var oauthBinding = new OAuth1Binding(config._consumerKey, config._secret, config._urls); - // If we get here with a callback url we need a request token to - // start the logic process - if (query.callbackUrl) { + if (query.requestTokenAndRedirect) { + // step 1 - get and store a request token // Get a request token to start auth process - oauth.getRequestToken(query.callbackUrl); + oauthBinding.prepareRequestToken(query.requestTokenAndRedirect); // Keep track of request token so we can verify it on the next step - Meteor.accounts.oauth1._requestTokens[query.state] = oauth.requestToken; + Meteor.accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken; - var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauth.requestToken; + // redirect to provider login, which will redirect back to "step 2" below + var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; res.writeHead(302, {'Location': redirectUrl}); res.end(); - // If we get here without a callback url we've just - // returned from authentication via the oauth provider - } else { + // step 2, redirected from provider login - complete the login + // process: if the user authorized permissions, get an access + // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it var requestToken = Meteor.accounts.oauth1._requestTokens[query.state]; delete Meteor.accounts.oauth1._requestTokens[query.state]; - // Verify user authorized access and the oauth_token matches + // Verify user authorized access and the oauth_token matches // the requestToken from previous step if (query.oauth_token && query.oauth_token === requestToken) { + // Prepare the login results before returning. This way the + // subsequent call to the `login` method will be immediate. + // Get the access token for signing requests - oauth.getAccessToken(query); + oauthBinding.prepareAccessToken(query); // Get or create user id - var oauthResult = service.handleOauthRequest(oauth); + var oauthResult = service.handleOauthRequest(oauthBinding); var userId = Meteor.accounts.updateOrCreateUser( oauthResult.options, oauthResult.extra); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index 1514d8ab1f..e7a004cc7a 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -6,8 +6,8 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { var twitterfooAccessToken = Meteor.uuid(); var twitterfooAccessTokenSecret = Meteor.uuid(); - OAuth1.prototype.getRequestToken = function() {}; - OAuth1.prototype.getAccessToken = function() { + OAuth1Binding.prototype.prepareRequestToken = function() {}; + OAuth1Binding.prototype.prepareAccessToken = function() { this.accessToken = twitterfooAccessToken; this.accessTokenSecret = twitterfooAccessTokenSecret; }; @@ -18,8 +18,6 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; - // XXX can we make this unnecessary? Not totally sold on _requireConfigs - // yet, but maybe I'm just being overly delicate. Meteor.accounts.twitterfoo = {}; Meteor.accounts.twitterfoo._requireConfigs = []; Meteor.accounts.twitterfoo._secret = 'XXX'; @@ -42,7 +40,7 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { // simulate logging in using twitterfoo Meteor.accounts.oauth1._requestTokens['STATE'] = twitterfooAccessToken; - + var req = { method: "POST", url: "/_oauth/twitterfoo?close", @@ -80,8 +78,6 @@ Tinytest.add("oauth1 - error in user creation", function (test) { var twitterfailAccessToken = Meteor.uuid(); var twitterfailAccessTokenSecret = Meteor.uuid(); - // XXX can we make this unnecessary? Not totally sold on _requireConfigs - // yet, but maybe I'm just being overly delicate. Meteor.accounts.twitterfail = {}; Meteor.accounts.twitterfail._requireConfigs = []; Meteor.accounts.twitterfail._secret = 'XXX'; diff --git a/packages/accounts-oauth1-helper/package.js b/packages/accounts-oauth1-helper/package.js index de76baa5d3..df47f39784 100644 --- a/packages/accounts-oauth1-helper/package.js +++ b/packages/accounts-oauth1-helper/package.js @@ -6,7 +6,7 @@ Package.describe({ Package.on_use(function (api) { api.use('accounts-oauth-helper', 'client'); - api.add_files('oauth1.js', 'server'); + api.add_files('oauth1_binding.js', 'server'); api.add_files('oauth1_common.js', ['client', 'server']); api.add_files('oauth1_server.js', 'server'); }); diff --git a/packages/accounts-oauth2-helper/oauth2_server.js b/packages/accounts-oauth2-helper/oauth2_server.js index 41919e92c0..718d7fd4b6 100644 --- a/packages/accounts-oauth2-helper/oauth2_server.js +++ b/packages/accounts-oauth2-helper/oauth2_server.js @@ -3,29 +3,26 @@ // connect middleware Meteor.accounts.oauth2._handleRequest = function (service, query, res) { - if (query.error) { - // The user didn't authorize access - return; + // check if user authorized access + if (!query.error) { + // Prepare the login results before returning. This way the + // subsequent call to the `login` method will be immediate. + + // Get or create user id + var oauthResult = service.handleOauthRequest(query); + + var userId = Meteor.accounts.updateOrCreateUser( + oauthResult.options, oauthResult.extra); + + // Generate and store a login token for reconnect + // XXX this could go in accounts_server.js instead + var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); + + // Store results to subsequent call to `login` + Meteor.accounts.oauth._loginResultForState[query.state] = + {token: loginToken, id: userId}; } - // Make sure we prepare the login results before returning. - // This way the subsequent call to the `login` method will be - // immediate. - - // Get or create user id - var oauthResult = service.handleOauthRequest(query); - - var userId = Meteor.accounts.updateOrCreateUser( - oauthResult.options, oauthResult.extra); - - // Generate and store a login token for reconnect - // XXX this could go in accounts_server.js instead - var loginToken = Meteor.accounts._loginTokens.insert({userId: userId}); - - // Store results to subsequent call to `login` - Meteor.accounts.oauth._loginResultForState[query.state] = - {token: loginToken, id: userId}; - // Either close the window, redirect, or render nothing // if all else fails Meteor.accounts.oauth._renderOauthResults(res, query); diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index d13328f4ba..1544225cd7 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -9,8 +9,6 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; - // XXX can we make this unnecessary? Not totally sold on _requireConfigs - // yet, but maybe I'm just being overly delicate. Meteor.accounts.foobook = {}; Meteor.accounts.foobook._requireConfigs = []; Meteor.accounts.foobook._secret = 'XXX'; @@ -54,7 +52,6 @@ Tinytest.add("oauth2 - error in user creation", function (test) { var email = Meteor.uuid() + "@example.com"; var state = Meteor.uuid(); - // XXX this shouldn't be necessary Meteor.accounts.failbook = {}; Meteor.accounts.failbook._requireConfigs = []; Meteor.accounts.failbook._secret = 'XXX'; diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index 8ecdafd6e2..d3407bb31e 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,14 +1,21 @@ (function () { Meteor.loginWithTwitter = function () { - if (!Meteor.accounts.twitter._appId || !Meteor.accounts.twitter._appUrl) + if (!Meteor.accounts.twitter._appUrl) throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first"); var state = Meteor.uuid(); // We need to keep state across the next two 'steps' so we're adding // a state parameter to the url and the callback url that we'll be returned // to by oauth provider + + // url back to app, enters "step 2" as described in + // packages/accounts-oauth1-helper/oauth1_server.js var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; - var url = '/_oauth/twitter/request_token?callbackUrl=' + encodeURIComponent(callbackUrl) + '&state=' + state + + // url to app, enters "step 1" as described in + // packages/accounts-oauth1-helper/oauth1_server.js + var url = '/_oauth/twitter/?requestTokenAndRedirect=' + encodeURIComponent(callbackUrl) + + '&state=' + state; Meteor.accounts.oauth.initiateLogin(state, url); }; diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index 812100b696..d09fc6c7aa 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,17 +1,17 @@ if (!Meteor.accounts.twitter) { Meteor.accounts.twitter = {}; - Meteor.accounts.twitter._requireConfigs = ['_appId', '_appUrl']; + Meteor.accounts.twitter._requireConfigs = ['_consumerKey', '_appUrl']; } -Meteor.accounts.twitter.config = function(appId, appUrl, options) { - Meteor.accounts.twitter._appId = appId; +Meteor.accounts.twitter.config = function(consumerKey, appUrl, options) { + Meteor.accounts.twitter._consumerKey = consumerKey; Meteor.accounts.twitter._appUrl = appUrl; - Meteor.accounts.twitter._options = options; - - Meteor.accounts.twitter._urls = { - requestToken: "https://api.twitter.com/oauth/request_token", - authorize: "https://api.twitter.com/oauth/authorize", - accessToken: "https://api.twitter.com/oauth/access_token", - authenticate: "https://api.twitter.com/oauth/authenticate" - }; + Meteor.accounts.twitter._options = options; // xcx don't need this +}; + +Meteor.accounts.twitter._urls = { + requestToken: "https://api.twitter.com/oauth/request_token", + authorize: "https://api.twitter.com/oauth/authorize", + accessToken: "https://api.twitter.com/oauth/access_token", + authenticate: "https://api.twitter.com/oauth/authenticate" }; diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index 2cdab4baa7..285e6fd34a 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,12 +1,11 @@ (function () { - Meteor.accounts.twitter.setSecret = function (secret) { - Meteor.accounts.twitter._secret = secret; + Meteor.accounts.twitter.setSecret = function (consumerSecret) { + Meteor.accounts.twitter._secret = consumerSecret; }; - Meteor.accounts.oauth.registerService('twitter', 1, function(oauth) { - - var identity = oauth.get('https://api.twitter.com/1/account/verify_credentials.json'); + Meteor.accounts.oauth.registerService('twitter', 1, function(oauthBinding) { + var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json'); return { options: { @@ -14,8 +13,8 @@ twitter: { id: identity.id, screenName: identity.screen_name, - accessToken: oauth.accessToken, - accessTokenSecret: oauth.accessTokenSecret + accessToken: oauthBinding.accessToken, + accessTokenSecret: oauthBinding.accessTokenSecret } } }, diff --git a/packages/accounts-ui/login_buttons.html b/packages/accounts-ui/login_buttons.html index 5dc344b2ae..26d60bee86 100644 --- a/packages/accounts-ui/login_buttons.html +++ b/packages/accounts-ui/login_buttons.html @@ -94,7 +94,7 @@ {{#if dropdownVisible}}
- + {{> loginButtonsServicesRow}}
From b9c3805f62ead7893527c38d7d19d788c04c22c1 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Fri, 24 Aug 2012 12:15:24 -0700 Subject: [PATCH 38/38] Code review changes + Configure todos to not require email so that Twitter login works --- examples/todos/accounts/config.js | 2 +- .../accounts-oauth-helper/oauth_server.js | 8 ++++ .../accounts-oauth1-helper/oauth1_binding.js | 39 +++++++++++-------- packages/accounts-twitter/twitter_client.js | 3 +- packages/accounts-twitter/twitter_common.js | 3 +- packages/accounts-ui/login_buttons.html | 2 +- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/examples/todos/accounts/config.js b/examples/todos/accounts/config.js index 3f1f07714f..55cd07f40f 100644 --- a/examples/todos/accounts/config.js +++ b/examples/todos/accounts/config.js @@ -1,5 +1,5 @@ Meteor.accounts.config({ - requireEmail: true, + requireEmail: false, requireUsername: false, validateEmails: true }); diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 7f2f3aa9aa..ca523b9143 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -105,6 +105,14 @@ // also log to the server console, so the developer sees it. Meteor._debug("Exception in oauth request handler", err); + // XXX the following is actually wrong. if someone wants to + // redirect rather than close once we are done with the OAuth + // flow, as supported by + // Meteor.accounts.oauth_renderOauthResults, this will still + // close the popup instead. Once we fully support the redirect + // flow (by supporting that in places such as + // packages/facebook/facebook_client.js) we should revisit this. + // // close the popup. because nobody likes them just hanging // there. when someone sees this multiple times they might // think to check server logs (we hope?) diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/accounts-oauth1-helper/oauth1_binding.js index 3cfb3d47a1..8f03ef8451 100644 --- a/packages/accounts-oauth1-helper/oauth1_binding.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -18,22 +18,25 @@ OAuth1Binding = function(consumerKey, consumerSecret, urls) { }; OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { + var self = this; - var headers = this._buildHeader({ + var headers = self._buildHeader({ oauth_callback: callbackUrl }); - var response = this._call('POST', this._urls.requestToken, headers); + var response = self._call('POST', self._urls.requestToken, headers); var tokens = querystring.parse(response.content); // XXX should we also store oauth_token_secret here? if (!tokens.oauth_callback_confirmed) throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens); - this.requestToken = tokens.oauth_token; + self.requestToken = tokens.oauth_token; }; OAuth1Binding.prototype.prepareAccessToken = function(query) { - var headers = this._buildHeader({ + var self = this; + + var headers = self._buildHeader({ oauth_token: query.oauth_token }); @@ -41,19 +44,21 @@ OAuth1Binding.prototype.prepareAccessToken = function(query) { oauth_verifier: query.oauth_verifier }; - var response = this._call('POST', this._urls.accessToken, headers, params); + var response = self._call('POST', self._urls.accessToken, headers, params); var tokens = querystring.parse(response.content); - this.accessToken = tokens.oauth_token; - this.accessTokenSecret = tokens.oauth_token_secret; + self.accessToken = tokens.oauth_token; + self.accessTokenSecret = tokens.oauth_token_secret; }; OAuth1Binding.prototype.call = function(method, url) { - var headers = this._buildHeader({ - oauth_token: this.accessToken + var self = this; + + var headers = self._buildHeader({ + oauth_token: self.accessToken }); - var response = this._call(method, url, headers); + var response = self._call(method, url, headers); return response.data; }; @@ -62,8 +67,9 @@ OAuth1Binding.prototype.get = function(url) { }; OAuth1Binding.prototype._buildHeader = function(headers) { + var self = this; return _.extend({ - oauth_consumer_key: this._consumerKey, + oauth_consumer_key: self._consumerKey, oauth_nonce: Meteor.uuid().replace(/\W/g, ''), oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), @@ -72,8 +78,8 @@ OAuth1Binding.prototype._buildHeader = function(headers) { }; OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, accessTokenSecret) { - - var headers = this._encodeHeader(rawHeaders); + var self = this; + var headers = self._encodeHeader(rawHeaders); var parameters = _.map(headers, function(val, key) { return key + '=' + val; @@ -85,7 +91,7 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access encodeURIComponent(parameters) ].join('&'); - var signingKey = encodeURIComponent(this._secret) + '&'; + var signingKey = encodeURIComponent(self._secret) + '&'; if (accessTokenSecret) signingKey += encodeURIComponent(accessTokenSecret); @@ -93,12 +99,13 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access }; OAuth1Binding.prototype._call = function(method, url, headers, params) { + var self = this; // Get the signature - headers.oauth_signature = this._getSignature(method, url, headers, this.accessTokenSecret); + headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret); // Make a authorization string according to oauth1 spec - var authString = this._getAuthHeaderString(headers); + var authString = self._getAuthHeaderString(headers); // Make signed request var response = Meteor.http.call(method, url, { diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index d3407bb31e..b75dcf378e 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -14,7 +14,8 @@ // url to app, enters "step 1" as described in // packages/accounts-oauth1-helper/oauth1_server.js - var url = '/_oauth/twitter/?requestTokenAndRedirect=' + encodeURIComponent(callbackUrl) + var url = '/_oauth/twitter/?requestTokenAndRedirect=' + + encodeURIComponent(callbackUrl) + '&state=' + state; Meteor.accounts.oauth.initiateLogin(state, url); diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index d09fc6c7aa..19fea5f6cb 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -3,10 +3,9 @@ if (!Meteor.accounts.twitter) { Meteor.accounts.twitter._requireConfigs = ['_consumerKey', '_appUrl']; } -Meteor.accounts.twitter.config = function(consumerKey, appUrl, options) { +Meteor.accounts.twitter.config = function(consumerKey, appUrl) { Meteor.accounts.twitter._consumerKey = consumerKey; Meteor.accounts.twitter._appUrl = appUrl; - Meteor.accounts.twitter._options = options; // xcx don't need this }; Meteor.accounts.twitter._urls = { diff --git a/packages/accounts-ui/login_buttons.html b/packages/accounts-ui/login_buttons.html index 26d60bee86..5dc344b2ae 100644 --- a/packages/accounts-ui/login_buttons.html +++ b/packages/accounts-ui/login_buttons.html @@ -94,7 +94,7 @@ {{#if dropdownVisible}}
- + {{> loginButtonsServicesRow}}