Support for Google login + refactor of accounts packages

Break up the accounts package into accounts, accounts-facebook, oauth2.
This commit is contained in:
Avital Oliver
2012-06-12 15:08:11 -07:00
committed by Nick Martin
parent d6bc56d255
commit 2fc45793ee
23 changed files with 471 additions and 248 deletions

View File

@@ -6,3 +6,5 @@
underscore
backbone
accounts
accounts-facebook
accounts-google

View File

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

View File

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

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

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

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

View 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;
};

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

View 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');
});

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

View 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;
};

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

View 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');
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1 @@
Meteor.accounts.oauth2 = {};

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

View 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');
});