Various resturcturing of the relationship between the different oauth packages

This commit is contained in:
Avital Oliver
2012-08-18 01:09:16 -07:00
committed by Nick Martin
parent 6b914593ae
commit 98c364338f
12 changed files with 145 additions and 134 deletions

View File

@@ -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);
};
})();

View File

@@ -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);

View File

@@ -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);
};
}) ();

View File

@@ -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);

View File

@@ -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;
};
})();
})();

View File

@@ -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/<service name>?<action>"
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();
});
};
})();

View File

@@ -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 =
'<html><head><script>window.close()</script></head></html>';
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 =
'<html><head><script>window.close()</script></head></html>';
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);
})();

View File

@@ -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 =
'<html><head><script>window.close()</script></head></html>';
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);
})();

View File

@@ -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);
};
})();

View File

@@ -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');

View File

@@ -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);
};
}) ();

View File

@@ -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));