diff --git a/examples/todos/.meteor/packages b/examples/todos/.meteor/packages
index df9943c56c..d35f3979f0 100644
--- a/examples/todos/.meteor/packages
+++ b/examples/todos/.meteor/packages
@@ -6,3 +6,5 @@
underscore
backbone
accounts
+accounts-facebook
+accounts-google
diff --git a/examples/todos/client/todos.html b/examples/todos/client/todos.html
index 3b7972a485..d5456fcd88 100644
--- a/examples/todos/client/todos.html
+++ b/examples/todos/client/todos.html
@@ -121,7 +121,8 @@
{{#if user}}
logout
{{else}}
- login using facebook
+ login using facebook
+ login using google
{{/if}}
diff --git a/examples/todos/client/todos.js b/examples/todos/client/todos.js
index cc0e2c731b..6f45cb1f71 100644
--- a/examples/todos/client/todos.js
+++ b/examples/todos/client/todos.js
@@ -307,12 +307,23 @@ Template.login.events = {
Meteor.loginWithFacebook();
} catch (e) {
if (e instanceof Meteor.accounts.facebook.SetupError)
- alert("You haven't set up your facebook app details. See fb-app.js and server/fb-secret.js");
+ alert("You haven't set up your Facebook app details. See fb-app.js and server/fb-secret.js");
else
throw e;
}
},
+ 'click #google-login': function () {
+ try {
+ Meteor.loginWithGoogle();
+ } catch (e) {
+ if (e instanceof Meteor.accounts.google.SetupError)
+ alert("You haven't set up your Google API details. See google-api.js and server/google-secret.js");
+ else
+ throw e;
+ };
+ },
+
'click #logout': function() {
Meteor.logout();
}
diff --git a/examples/todos/google-api.js b/examples/todos/google-api.js
new file mode 100644
index 0000000000..7639342f02
--- /dev/null
+++ b/examples/todos/google-api.js
@@ -0,0 +1,4 @@
+// Uncomment and correct following line for integration with Google accounts.
+// Also see server/google-secret.js
+
+// Meteor.accounts.google.setup('987846107089.apps.googleusercontent.com', 'http://auth-todos.meteor.com');
diff --git a/examples/todos/server/google-secret.js b/examples/todos/server/google-secret.js
new file mode 100644
index 0000000000..822ca36683
--- /dev/null
+++ b/examples/todos/server/google-secret.js
@@ -0,0 +1,4 @@
+// Uncomment and correct following line for integration with Google accounts.
+// Also see ../google-api.js
+
+// Meteor.accounts.google.setSecret('SECRET');
\ No newline at end of file
diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js
new file mode 100644
index 0000000000..60cca7f60f
--- /dev/null
+++ b/packages/accounts-facebook/facebook_client.js
@@ -0,0 +1,21 @@
+(function () {
+ Meteor.loginWithFacebook = function () {
+ if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
+ throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first");
+
+ var state = Meteor.uuid();
+ // XXX I think there's a smaller popup. Replace with appropriate URL.
+ // XXX need to support configuring scope
+ var loginUrl =
+ 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId +
+ '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' +
+ '&scope=email&state=' + state;
+
+ Meteor.accounts.oauth2.initiateLogin(state, loginUrl);
+ };
+
+})();
+
+
+
+
diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js
new file mode 100644
index 0000000000..f272f8a89c
--- /dev/null
+++ b/packages/accounts-facebook/facebook_common.js
@@ -0,0 +1,12 @@
+if (!Meteor.accounts.facebook) {
+ Meteor.accounts.facebook = {};
+}
+
+Meteor.accounts.facebook.setup = function(appId, appUrl) {
+ Meteor.accounts.facebook._appId = appId;
+ Meteor.accounts.facebook._appUrl = appUrl;
+};
+
+Meteor.accounts.facebook.SetupError = function(description) {
+ this.message = description;
+};
diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js
new file mode 100644
index 0000000000..51efee1cb8
--- /dev/null
+++ b/packages/accounts-facebook/facebook_server.js
@@ -0,0 +1,79 @@
+(function () {
+
+ Meteor.accounts.facebook.setSecret = function (secret) {
+ Meteor.accounts.facebook._secret = secret;
+ };
+
+ // register the facebook identity provider
+ Meteor.accounts.oauth2.providers.facebook = {
+ userIdForOauthReq: function(req) {
+ if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
+ throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first");
+ if (!Meteor.accounts.facebook._secret)
+ throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first");
+
+ var accessToken = getAccessToken(req);
+ // If the user didn't authorize the login, either explicitly
+ // or by closing the popup window, return null
+ if (!accessToken)
+ return null;
+
+ // Fetch user's facebook identity
+ var identity = Meteor.http.get("https://graph.facebook.com/me", {
+ params: {access_token: accessToken}}).data;
+
+ return Meteor.accounts.updateOrCreateUser(
+ identity.email, 'facebook', identity.id,
+ {accessToken: accessToken});
+ }
+ };
+
+ // @returns {String} Facebook access token
+ var getAccessToken = function (req) {
+ if (req.query.error) {
+ // The user didn't authorize access
+ return null;
+ }
+
+ // Request an access token
+ var response = Meteor.http.get(
+ "https://graph.facebook.com/oauth/access_token", {
+ params: {
+ client_id: Meteor.accounts.facebook._appId,
+ redirect_uri: Meteor.accounts.facebook._appUrl + "/_oauth/facebook?close",
+ client_secret: Meteor.accounts.facebook._secret,
+ code: req.query.code
+ }
+ }).content;
+
+ // Errors come back as JSON but success looks like a query encoded in a url
+ var error_response;
+ try {
+ // Just try to parse so that we know if we failed or not,
+ // while storing the parsed results
+ error_response = JSON.parse(response);
+ } catch (e) {
+ error_response = null;
+ }
+
+ if (error_response) {
+ if (error_response.error) {
+ throw new Meteor.Error("Error trying to get access token from Facebook", error_response);
+ } else {
+ throw new Meteor.Error("Unexpected response when trying to get access token from Facebook", error_response);
+ }
+ } else {
+ // Success! Extract the facebook access token from the
+ // response
+ var fbAccessToken;
+ _.each(response.split('&'), function(kvString) {
+ var kvArray = kvString.split('=');
+ if (kvArray[0] === 'access_token')
+ fbAccessToken = kvArray[1];
+ // XXX also parse the "expires" argument?
+ });
+
+ return fbAccessToken;
+ }
+ };
+}) ();
\ No newline at end of file
diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js
new file mode 100644
index 0000000000..49ce3d95bd
--- /dev/null
+++ b/packages/accounts-facebook/package.js
@@ -0,0 +1,13 @@
+Package.describe({
+ summary: "Integration with facebook accounts",
+});
+
+Package.on_use(function(api) {
+ api.use('accounts', ['client', 'server']);
+ api.use('oauth2', ['client', 'server']);
+ api.use('http', ['client', 'server']);
+
+ api.add_files('facebook_common.js', ['client', 'server']);
+ api.add_files('facebook_server.js', 'server');
+ api.add_files('facebook_client.js', 'client');
+});
diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js
new file mode 100644
index 0000000000..7bb0ed49fa
--- /dev/null
+++ b/packages/accounts-google/google_client.js
@@ -0,0 +1,19 @@
+(function () {
+ Meteor.loginWithGoogle = function () {
+ if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl)
+ throw new Meteor.accounts.google.SetupError("Need to call Meteor.accounts.google.setup first");
+
+ var state = Meteor.uuid();
+ // XXX need to support configuring access_type and scopy
+ var loginUrl =
+ 'https://accounts.google.com/o/oauth2/auth' +
+ '?response_type=code' +
+ '&client_id=' + Meteor.accounts.google._clientId +
+ '&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile' +
+ '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' +
+ '&state=' + state;
+
+ Meteor.accounts.oauth2.initiateLogin(state, loginUrl);
+ };
+
+}) ();
diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js
new file mode 100644
index 0000000000..18ce2c56fc
--- /dev/null
+++ b/packages/accounts-google/google_common.js
@@ -0,0 +1,12 @@
+if (!Meteor.accounts.google) {
+ Meteor.accounts.google = {};
+}
+
+Meteor.accounts.google.setup = function(clientId, appUrl) {
+ Meteor.accounts.google._clientId = clientId;
+ Meteor.accounts.google._appUrl = appUrl;
+};
+
+Meteor.accounts.google.SetupError = function(description) {
+ this.message = description;
+};
diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js
new file mode 100644
index 0000000000..af40eb392c
--- /dev/null
+++ b/packages/accounts-google/google_server.js
@@ -0,0 +1,45 @@
+(function () {
+ Meteor.accounts.google.setSecret = function (secret) {
+ Meteor.accounts.google._secret = secret;
+ };
+
+ Meteor.accounts.oauth2.providers.google = {
+ userIdForOauthReq: function(req) {
+ var accessToken = getAccessToken(req);
+
+ // XXX can we generalize this flow into the oauth abstraction?
+ if (!accessToken)
+ return null;
+
+ var identity = Meteor.http.get(
+ "https://www.googleapis.com/oauth2/v1/userinfo",
+ {params: {access_token: accessToken}}).data;
+
+ return Meteor.accounts.updateOrCreateUser(
+ identity.email, 'google', identity.id,
+ {accessToken: accessToken});
+ }
+ };
+
+ var getAccessToken = function (req) {
+ if (req.query.error) {
+ // The user didn't authorize access
+ // XXX can we generalize this into the oauth abstration?
+ return null;
+ }
+
+ var response = Meteor.http.post(
+ "https://accounts.google.com/o/oauth2/token", {params: {
+ code: req.query.code,
+ 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'
+ }}).data;
+
+ if (response.error)
+ throw response;
+
+ return response.access_token;
+ };
+})();
\ No newline at end of file
diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js
new file mode 100644
index 0000000000..ca4119491d
--- /dev/null
+++ b/packages/accounts-google/package.js
@@ -0,0 +1,13 @@
+Package.describe({
+ summary: "Integration with google accounts",
+});
+
+Package.on_use(function(api) {
+ api.use('accounts', ['client', 'server']);
+ api.use('oauth2', ['client', 'server']);
+ api.use('http', ['client', 'server']);
+
+ api.add_files('google_common.js', ['client', 'server']);
+ api.add_files('google_server.js', 'server');
+ api.add_files('google_client.js', 'client');
+});
diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js
index ecb3644b9d..1f93cfa5ac 100644
--- a/packages/accounts/accounts_client.js
+++ b/packages/accounts/accounts_client.js
@@ -1,4 +1,4 @@
-(function() {
+(function () {
Meteor.user = function () {
if (Meteor.default_connection.userId()) {
// XXX full identity?
@@ -14,74 +14,12 @@
});
}
- Meteor.loginWithFacebook = function () {
- 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;
- };
-
- if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
- throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first");
-
- var oauthState = Meteor.uuid();
-
- var popup = openCenteredPopup(
- 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId +
- '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' +
- '&scope=email&state=' + oauthState,
- 1000, 600); // XXX should we use different dimensions, e.g. on mobile?
-
- var checkPopupOpen = setInterval(function() {
- if (popup.closed) {
- clearInterval(checkPopupOpen);
- tryLoginAfterPopupClosed(oauthState);
- }
- }, 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(oauthState) {
- Meteor.apply('login', [
- {oauth: {version: 2, provider: 'facebook', state: oauthState}}
- ], {wait: true}, function(error, result) {
- if (error) {
- Meteor._debug("Server error on login", error);
- return;
- }
-
- Meteor.accounts.loginAndStoreToken(result.token);
- callback && callback();
- });
- };
-
Meteor.logout = function () {
Meteor.apply('logout', [], {wait: true}, function(error, result) {
- if (error) {
- Meteor._debug("Server error on logout", error);
- return;
- } else {
+ if (error)
+ throw error;
+ else
Meteor.accounts.forceClientLoggedOut();
- }
});
};
})();
diff --git a/packages/accounts/accounts_common.js b/packages/accounts/accounts_common.js
index 9a15b34269..297949567b 100644
--- a/packages/accounts/accounts_common.js
+++ b/packages/accounts/accounts_common.js
@@ -4,21 +4,8 @@ if (!Meteor.accounts) {
Meteor.accounts = {};
}
-if (!Meteor.accounts.facebook) {
- Meteor.accounts.facebook = {};
-}
-
Meteor.accounts._loginTokens = new Meteor.Collection(
"accounts._loginTokens",
null /*manager*/,
null /*driver*/,
true /*preventAutopublish*/);
-
-Meteor.accounts.facebook.setup = function(appId, appUrl) {
- Meteor.accounts.facebook._appId = appId;
- Meteor.accounts.facebook._appUrl = appUrl;
-};
-
-Meteor.accounts.facebook.SetupError = function(description) {
- this.message = description;
-};
diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js
index 81571f4b7e..a4fc96acfb 100644
--- a/packages/accounts/accounts_server.js
+++ b/packages/accounts/accounts_server.js
@@ -1,53 +1,65 @@
-(function() {
+(function () {
+ // Updates or creates a user after we authenticate with a 3rd party
+ // @param serviceName {String} e.g. 'facebook' or 'google'
+ // @param serviceUserId {?} user id in 3rd party service
+ // @param more {Object} additional attributes to store on the user record
+ // @returns {String} userId
+ Meteor.accounts.updateOrCreateUser = function(email,
+ serviceName,
+ serviceUserId,
+ more) {
- var connect = __meteor_bootstrap__.require("connect");
+ var userByEmail = Meteor.users.findOne({emails: email});
+ if (userByEmail) {
- // Incoming OAuth http requests are recorded here when the OAuth
- // process is completed inside a popup window. Afterwards, these are
- // read by the OAuth login method to complete the process.
- //
- // @type {Object} maps from Oauth "state" to request
- Meteor.accounts._unmatchedOauthRequests = {};
+ // If we know about this email address that is our user.
+ // Update the information from this service.
+ var user = userByEmail;
+ if (!user.services || !user.services[serviceName]) {
+ var attrs = {};
+ attrs["services." + serviceName] = _.extend(
+ {id: serviceUserId}, more);
+ Meteor.users.update(user, {$set: attrs});
+ }
+ return user._id;
+ } else {
- Meteor.accounts.facebook.setSecret = function(secret) {
- Meteor.accounts.facebook._secret = secret;
+ // If not, look for a user with the appropriate service user id.
+ // Update the user's email.
+ var selector = {};
+ selector["services." + serviceName + ".id"] = serviceUserId;
+ var userByServiceUserId = Meteor.users.findOne(selector);
+ if (userByServiceUserId) {
+ var user = userByServiceUserId;
+ if (user.emails.indexOf(email) === -1) {
+ // The user may have changed the email address associated with
+ // this service. Store the new one in addition to the old one.
+ Meteor.users.update(user, {$push: {emails: email}});
+ }
+ return user._id;
+ } else {
+
+ // Create a new user
+ var attrs = {};
+ attrs[serviceName] = _.extend({id: serviceUserId}, more);
+ return Meteor.users.insert({
+ emails: [email],
+ services: attrs
+ });
+ }
+ }
};
- // Listen on /_oauth/*
- __meteor_bootstrap__.app
- .use(connect.query())
- .use(function (req, res, next) {
- Fiber(function() {
- // Any non-oauth request will continue down the default middlewares
- if (req.url.split('/')[1] !== '_oauth') {
- next();
- return;
- }
+ Meteor.accounts._loginHandlers = [];
- if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
- throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setup first");
- if (!Meteor.accounts.facebook._secret)
- throw new Meteor.accounts.facebook.SetupError("Need to call Meteor.accounts.facebook.setSecret first");
-
- Meteor.accounts._unmatchedOauthRequests[req.query.state] = req;
-
- // We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request
- // just served 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(content, 'utf-8');
- }
- }).run();
- });
+ // @param handler {Function} A function that receives an options object
+ // (as passed as an argument to the `login` method) and returns one of:
+ // - `undefined`, meaning don't handle;
+ // - `null`, meaning the user didn't actually log in;
+ // - {id: userId, accessToken: *}, if the user logged in successfully.
+ Meteor.accounts.registerLoginHandler = function(handler) {
+ Meteor.accounts._loginHandlers.push(handler);
+ };
Meteor.methods({
// @returns {Object|null}
@@ -55,73 +67,9 @@
// If unsuccessful (for example, if the user closed the oauth login popup),
// returns null
login: function(options) {
- // XXX write test for updateOrCreateUser
- var updateOrCreateUser = function(email, fbId, fbAccessToken) {
- var userByEmail = Meteor.users.findOne({emails: email});
- if (userByEmail) {
- var user = userByEmail;
- if (!user.services || !user.services.facebook)
- Meteor.users.update(user, {$set: {"services.facebook": {
- id: fbId,
- accessToken: fbAccessToken
- }}});
- return user._id;
- } else {
- var userByFacebookId = Meteor.users.findOne({"services.facebook.id": fbId});
- if (userByFacebookId) {
- var user = userByFacebookId;
- if (user.emails.indexOf(email) === -1) {
- // The user may have changed the email address associated with
- // their facebook account.
- Meteor.users.update(user, {$push: {emails: email}});
- }
- return user._id;
- } else {
- return Meteor.users.insert({
- emails: [email],
- services: {
- facebook: {id: fbId, accessToken: fbAccessToken}
- }
- });
- }
- }
- };
-
- if (options.oauth) {
- if (options.oauth.version !== 2 || options.oauth.provider !== 'facebook')
- throw new Meteor.Error("We only support facebook login for now. More soon!");
-
- var fbAccessToken;
- var unmatchedRequest = Meteor.accounts._unmatchedOauthRequests[options.oauth.state];
- if (unmatchedRequest) {
- // We had previously received the HTTP request with the OAuth code
- fbAccessToken = handleOauthRequest(unmatchedRequest);
- delete Meteor.accounts._unmatchedOauthRequests[options.oauth.state];
-
- // If the user didn't authorize the login, either explicitly
- // or by closing the popup window, return null
- if (!fbAccessToken)
- return null;
- } else {
- return null;
- }
-
- // Fetch user's facebook identity
- var identity = Meteor.http.get(
- "https://graph.facebook.com/me?access_token=" + fbAccessToken).data;
- this.setUserId(updateOrCreateUser(identity.email, identity.id, fbAccessToken));
-
- // Generate and store a login token for reconnect
- var loginToken = Meteor.accounts._loginTokens.insert({
- userId: this.userId()
- });
-
- return {
- token: loginToken,
- id: this.userId()
- };
- } else if (options.resume) {
- var loginToken = Meteor.accounts._loginTokens.findOne({_id: options.resume});
+ if (options.resume) {
+ var loginToken = Meteor.accounts._loginTokens
+ .findOne({_id: options.resume});
if (!loginToken)
throw new Meteor.Error("Couldn't find login token");
this.setUserId(loginToken.userId);
@@ -131,7 +79,10 @@
id: this.userId()
};
} else {
- throw new Meteor.Error("Unrecognized options for login request");
+ var result = tryAllLoginHandlers(options);
+ if (result !== null)
+ this.setUserId(result.id);
+ return result;
}
},
@@ -140,57 +91,28 @@
}
});
- // @returns {String} Facebook access token
- var handleOauthRequest = function(req) {
- var bareUrl = req.url.substring(0, req.url.indexOf('?'));
- var provider = bareUrl.split('/')[2];
- if (provider === 'facebook') {
- if (req.query.error) {
- // Either the user didn't authorize access or we cancelled
- // this outstanding login request (such as when the user
- // closes the login popup window)
- return null;
- }
+ // Try all of the registered login handlers until one of them doesn't
+ // return `undefined`, meaning it handled this call to `login`. Return
+ // that return value.
+ var tryAllLoginHandlers = function (options) {
+ var result = undefined;
- // Request an access token
- var response = Meteor.http.get(
- "https://graph.facebook.com/oauth/access_token?" +
- "client_id=" + Meteor.accounts.facebook._appId +
- "&redirect_uri=" + Meteor.accounts.facebook._appUrl + "/_oauth/facebook?close" +
- "&client_secret=" + Meteor.accounts.facebook._secret +
- "&code=" + req.query.code).content;
+ _.find(Meteor.accounts._loginHandlers, function(handler) {
- // Errors come back as JSON but success looks like a query encoded in a url
- var error_response = null;
- try {
- // Just try to parse so that we know if we failed or not,
- // while storing the parsed results
- var error_response = JSON.parse(response);
- } catch (e) {
- }
-
- if (error_response) {
- if (error_response.error) {
- throw new Meteor.Error("Error trying to get access token from Facebook", error_response);
- } else {
- throw new Meteor.Error("Unexpected response when trying to get access token from Facebook", error_response);
- }
+ var maybeResult = handler(options);
+ if (maybeResult !== undefined) {
+ result = maybeResult;
+ return true;
} else {
- // Success! Extract the facebook access token from the
- // response
- var fbAccessToken;
- _.each(response.split('&'), function(kvString) {
- var kvArray = kvString.split('=');
- if (kvArray[0] === 'access_token')
- fbAccessToken = kvArray[1];
- // XXX also parse the "expires" argument?
- });
-
- return fbAccessToken;
+ return false;
}
+ });
+
+ if (result === undefined) {
+ throw new Meteor.Error("Unrecognized options for login request");
} else {
- throw new Meteor.Error("Unknown OAuth provider: " + provider);
+ return result;
}
};
-})();
+}) ();
diff --git a/packages/accounts/localstorage_token.js b/packages/accounts/localstorage_token.js
index 781ab3dbc1..b59071020e 100644
--- a/packages/accounts/localstorage_token.js
+++ b/packages/accounts/localstorage_token.js
@@ -28,18 +28,17 @@ Meteor.loginFromLocalStorage = function () {
Meteor.accounts._lastLoginTokenWhenPolled = loginToken;
if (loginToken) {
Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) {
- if (error) {
- Meteor._debug("Server error on login", error);
- return;
- }
+ if (error)
+ throw error;
Meteor.default_connection.setUserId(result.id);
Meteor.default_connection.onReconnect = function() {
Meteor.apply('login', [{resume: loginToken}], {wait: true}, function(error, result) {
if (error) {
Meteor.accounts.forceClientLoggedOut();
- Meteor._debug("Server error on login", error);
- return;
+ throw error;
+ } else {
+ // nothing to do
}
});
};
diff --git a/packages/accounts/package.js b/packages/accounts/package.js
index 1da6e15120..8b189d48f4 100644
--- a/packages/accounts/package.js
+++ b/packages/accounts/package.js
@@ -3,7 +3,7 @@ Package.describe({
});
Package.on_use(function(api) {
- api.use('http', ['client', 'server']);
+ api.use('underscore', 'server');
api.use('localstorage-polyfill', 'client');
api.add_files('accounts_common.js', ['client', 'server']);
diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js
index d8d473136e..c3b83d403b 100644
--- a/packages/localstorage-polyfill/localstorage_polyfill.js
+++ b/packages/localstorage-polyfill/localstorage_polyfill.js
@@ -1,6 +1,7 @@
Meteor.startup(function() { // Since we need document.body to be defined
if (!window.localStorage) {
window.localStorage = (function () {
+ // XXX eliminate dependency on jQuery, detect browsers ourselves
if ($.browser.msie) { // If we are on IE, which support userData
var userdata = document.createElement('span'); // could be anything
userdata.style.behavior = 'url("#default#userData")';
@@ -30,8 +31,8 @@ Meteor.startup(function() { // Since we need document.body to be defined
} else {
Meteor._debug(
"You are running a browser with no localStorage or userData "
- + "support (presumable Opera Mini). Logging in from one tab "
- + "will not cause another tab to be logged in.");
+ + "support. Logging in from one tab will not cause another "
+ + "tab to be logged in.");
return {
setItem: function () {},
diff --git a/packages/oauth2/oauth2_client.js b/packages/oauth2/oauth2_client.js
new file mode 100644
index 0000000000..a1d13f9580
--- /dev/null
+++ b/packages/oauth2/oauth2_client.js
@@ -0,0 +1,56 @@
+(function () {
+ Meteor.accounts.oauth2.initiateLogin = function(state, url) {
+ // XXX should we use different dimensions, e.g. on mobile?
+ var popup = openCenteredPopup(url, 1000, 600);
+
+ var checkPopupOpen = setInterval(function() {
+ if (popup.closed) {
+ clearInterval(checkPopupOpen);
+ tryLoginAfterPopupClosed(state);
+ }
+ }, 100);
+ };
+
+ 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;
+ };
+
+ // 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(oauthState) {
+ Meteor.apply('login', [
+ {oauth: {version: 2, state: oauthState}}
+ ], {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.loginAndStoreToken(result.token);
+ }
+ });
+ };
+})();
\ No newline at end of file
diff --git a/packages/oauth2/oauth2_common.js b/packages/oauth2/oauth2_common.js
new file mode 100644
index 0000000000..cb23a48c2d
--- /dev/null
+++ b/packages/oauth2/oauth2_common.js
@@ -0,0 +1 @@
+Meteor.accounts.oauth2 = {};
\ No newline at end of file
diff --git a/packages/oauth2/oauth2_server.js b/packages/oauth2/oauth2_server.js
new file mode 100644
index 0000000000..02ad2308e4
--- /dev/null
+++ b/packages/oauth2/oauth2_server.js
@@ -0,0 +1,71 @@
+(function () {
+ var connect = __meteor_bootstrap__.require("connect");
+
+ Meteor.accounts.oauth2.providers = {};
+
+ Meteor.accounts.registerLoginHandler(function (options) {
+ if (!options.oauth)
+ 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
+ return result;
+ });
+
+ // When we get an incoming OAuth http request we complete the
+ // facebook 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 {oauthState} --> return value of
+ // `login`
+ Meteor.accounts.oauth2.loginResultForState = {};
+
+ // Listen on /_oauth/*
+ __meteor_bootstrap__.app
+ .use(connect.query())
+ .use(function (req, res, next) {
+ Fiber(function() {
+ var bareUrl = req.url.substring(0, req.url.indexOf('?'));
+ var splitUrl = bareUrl.split('/');
+
+ // Any non-oauth request will continue down the default middlewares
+ if (splitUrl[1] !== '_oauth') {
+ next();
+ return;
+ }
+
+ // Make sure we prepare the login results before returning.
+ // This way the subsequent call to the `login` method will be
+ // immediate.
+
+ var providerName = splitUrl[2];
+ var provider = Meteor.accounts.oauth2.providers[providerName];
+ // Get or create user id
+ var userId = provider.userIdForOauthReq(req);
+ // Generate and store a login token for reconnect
+ 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};
+
+ // We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request
+ // just served 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(content, 'utf-8');
+ }
+ }).run();
+ });
+
+})();
\ No newline at end of file
diff --git a/packages/oauth2/package.js b/packages/oauth2/package.js
new file mode 100644
index 0000000000..1a184ded51
--- /dev/null
+++ b/packages/oauth2/package.js
@@ -0,0 +1,12 @@
+Package.describe({
+ summary: "A basis for OAuth2-based account systems",
+});
+
+Package.on_use(function (api) {
+ api.use('jquery', 'client'); // XXX only used for browser detection. remove.
+ api.use('accounts', ['client', 'server']);
+
+ api.add_files('oauth2_common.js', ['client', 'server']);
+ api.add_files('oauth2_server.js', 'server');
+ api.add_files('oauth2_client.js', 'client');
+});