mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Support for Google login + refactor of accounts packages
Break up the accounts package into accounts, accounts-facebook, oauth2.
This commit is contained in:
committed by
Nick Martin
parent
d6bc56d255
commit
2fc45793ee
@@ -6,3 +6,5 @@
|
||||
underscore
|
||||
backbone
|
||||
accounts
|
||||
accounts-facebook
|
||||
accounts-google
|
||||
|
||||
@@ -121,7 +121,8 @@
|
||||
{{#if user}}
|
||||
<div id="logout">logout</div>
|
||||
{{else}}
|
||||
<div id="fb-login" class="fb-login">login using facebook</div>
|
||||
<div id="fb-login">login using facebook</div>
|
||||
<div id="google-login">login using google</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
4
examples/todos/google-api.js
Normal file
4
examples/todos/google-api.js
Normal file
@@ -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');
|
||||
4
examples/todos/server/google-secret.js
Normal file
4
examples/todos/server/google-secret.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Uncomment and correct following line for integration with Google accounts.
|
||||
// Also see ../google-api.js
|
||||
|
||||
// Meteor.accounts.google.setSecret('SECRET');
|
||||
21
packages/accounts-facebook/facebook_client.js
Normal file
21
packages/accounts-facebook/facebook_client.js
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
|
||||
12
packages/accounts-facebook/facebook_common.js
Normal file
12
packages/accounts-facebook/facebook_common.js
Normal file
@@ -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;
|
||||
};
|
||||
79
packages/accounts-facebook/facebook_server.js
Normal file
79
packages/accounts-facebook/facebook_server.js
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
}) ();
|
||||
13
packages/accounts-facebook/package.js
Normal file
13
packages/accounts-facebook/package.js
Normal file
@@ -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');
|
||||
});
|
||||
19
packages/accounts-google/google_client.js
Normal file
19
packages/accounts-google/google_client.js
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
}) ();
|
||||
12
packages/accounts-google/google_common.js
Normal file
12
packages/accounts-google/google_common.js
Normal file
@@ -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;
|
||||
};
|
||||
45
packages/accounts-google/google_server.js
Normal file
45
packages/accounts-google/google_server.js
Normal file
@@ -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;
|
||||
};
|
||||
})();
|
||||
13
packages/accounts-google/package.js
Normal file
13
packages/accounts-google/package.js
Normal file
@@ -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');
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 =
|
||||
'<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'});
|
||||
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;
|
||||
}
|
||||
};
|
||||
})();
|
||||
}) ();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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 () {},
|
||||
|
||||
56
packages/oauth2/oauth2_client.js
Normal file
56
packages/oauth2/oauth2_client.js
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
1
packages/oauth2/oauth2_common.js
Normal file
1
packages/oauth2/oauth2_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Meteor.accounts.oauth2 = {};
|
||||
71
packages/oauth2/oauth2_server.js
Normal file
71
packages/oauth2/oauth2_server.js
Normal file
@@ -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 =
|
||||
'<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'});
|
||||
res.end(content, 'utf-8');
|
||||
}
|
||||
}).run();
|
||||
});
|
||||
|
||||
})();
|
||||
12
packages/oauth2/package.js
Normal file
12
packages/oauth2/package.js
Normal file
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user