From 98c364338f12f3eeaa573210aa09e935b71fbd6a Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Sat, 18 Aug 2012 01:09:16 -0700 Subject: [PATCH] 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));