Better interface for OAuth2 login services + more cleanup

This commit is contained in:
Avital Oliver
2012-06-15 13:45:36 -07:00
committed by Nick Martin
parent 0d886d6c24
commit a73715491b
9 changed files with 119 additions and 77 deletions

View File

@@ -1,7 +1,7 @@
(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");
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setup first");
var state = Meteor.uuid();
// XXX I think there's a smaller popup. Replace with appropriate URL.

View File

@@ -2,11 +2,9 @@ if (!Meteor.accounts.facebook) {
Meteor.accounts.facebook = {};
}
Meteor.accounts.facebook.setup = function(appId, appUrl) {
Meteor.accounts.facebook.config = function(appId, appUrl) {
Meteor.accounts.facebook._appId = appId;
Meteor.accounts.facebook._appUrl = appUrl;
};
Meteor.accounts.facebook.SetupError = function(description) {
this.message = description;
};

View File

@@ -4,37 +4,30 @@
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, {name: identity.name},
'facebook', identity.id, {accessToken: accessToken});
}
};
// @returns {String} Facebook access token
var getAccessToken = function (req) {
if (req.query.error) {
Meteor.accounts.oauth2.registerService('facebook', function(query) {
if (query.error) {
// The user didn't authorize access
// XXX can/should we generalize this into the oauth abstration?
return null;
}
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setup first");
if (!Meteor.accounts.facebook._secret)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setSecret first");
var accessToken = getAccessToken(query);
var identity = getIdentity(accessToken);
return {
email: identity.email,
userData: {name: identity.name},
serviceUserId: identity.id,
serviceData: {accessToken: accessToken}
};
});
var getAccessToken = function (query) {
// Request an access token
var response = Meteor.http.get(
"https://graph.facebook.com/oauth/access_token", {
@@ -42,7 +35,7 @@
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
code: query.code
}
}).content;
@@ -76,4 +69,13 @@
return fbAccessToken;
}
};
var getIdentity = function (accessToken) {
var result = Meteor.http.get("https://graph.facebook.com/me", {
params: {access_token: accessToken}});
if (result.error)
throw result.error;
return result.data;
};
}) ();

View File

@@ -4,7 +4,7 @@
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
// XXX need to support configuring access_type and scope
var loginUrl =
'https://accounts.google.com/o/oauth2/auth' +
'?response_type=code' +

View File

@@ -2,11 +2,7 @@ if (!Meteor.accounts.google) {
Meteor.accounts.google = {};
}
Meteor.accounts.google.setup = function(clientId, appUrl) {
Meteor.accounts.google.config = function(clientId, appUrl) {
Meteor.accounts.google._clientId = clientId;
Meteor.accounts.google._appUrl = appUrl;
};
Meteor.accounts.google.SetupError = function(description) {
this.message = description;
};

View File

@@ -1,45 +1,56 @@
(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, {name: identity.name},
'google', identity.id, {accessToken: accessToken});
}
};
var getAccessToken = function (req) {
if (req.query.error) {
Meteor.accounts.oauth2.registerService('google', function(query) {
if (query.error) {
// The user didn't authorize access
// XXX can we generalize this into the oauth abstration?
// XXX can/should we generalize this into the oauth abstration?
return null;
}
var response = Meteor.http.post(
if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first");
if (!Meteor.accounts.google._secret)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.setSecret first");
var accessToken = getAccessToken(query);
var identity = getIdentity(accessToken);
return {
email: identity.email,
userData: {name: identity.name},
serviceUserId: identity.id,
serviceData: {accessToken: accessToken}
};
});
var getAccessToken = function (query) {
var result = Meteor.http.post(
"https://accounts.google.com/o/oauth2/token", {params: {
code: req.query.code,
code: 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;
if (result.error) // if the http response was an error
throw result.error;
if (result.data.error) // if the http response was a json object with an error attribute
throw result.data;
return result.data.access_token;
};
return response.access_token;
var getIdentity = function (accessToken) {
var result = Meteor.http.get(
"https://www.googleapis.com/oauth2/v1/userinfo",
{params: {access_token: accessToken}});
if (result.error)
throw result.error;
return result.data;
};
})();

View File

@@ -5,8 +5,8 @@
try {
Meteor.loginWithFacebook();
} catch (e) {
if (e instanceof Meteor.accounts.facebook.SetupError)
alert("Facebook API key not set. Configure app details with Meteor.accounts.facebook.setup()");
if (e instanceof Meteor.accounts.ConfigError)
alert("Facebook API key not set. Configure app details with Meteor.accounts.facebook.config()");
else
throw e;
}
@@ -16,8 +16,8 @@
try {
Meteor.loginWithGoogle();
} catch (e) {
if (e instanceof Meteor.accounts.google.SetupError)
alert("Google API key not set. Configure app details with Meteor.accounts.google.setup()");
if (e instanceof Meteor.accounts.ConfigError)
alert("Google API key not set. Configure app details with Meteor.accounts.google.config()");
else
throw e;
};

View File

@@ -16,3 +16,8 @@ Meteor.users = new Meteor.Collection(
null /*manager*/,
null /*driver*/,
true /*preventAutopublish*/);
// Thrown when trying to use a login service which is not configured
Meteor.accounts.ConfigError = function(description) {
this.message = description;
};

View File

@@ -1,13 +1,38 @@
(function () {
var connect = __meteor_bootstrap__.require("connect");
Meteor.accounts.oauth2.providers = {};
Meteor.accounts.oauth2._services = {};
// Register a handler for an OAuth2 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 handleOauthRequest {Function(query): userInfo}
// - query is an object with the parameters passed in the query string
// - userInfo {Object} with following keys:
// - email {String}
// - userData {Object} attributes to store directly on the user object,
// such as "name"
// - serviceUserId {?} The logging in user's id in the login service
// - serviceData {Object} attributes to store on the user record's
// specific login service's subobject, such as
// "accessToken"
Meteor.accounts.oauth2.registerService = function (name, handleOauthRequest) {
if (Meteor.accounts.oauth2._services[name])
throw new Meteor.Error("Already registered the " + name + " OAuth2 service");
Meteor.accounts.oauth2._services[name] = {
handleOauthRequest: handleOauthRequest
};
};
// 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 = Meteor.accounts.oauth2.loginResultForState[options.oauth.state];
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;
@@ -20,7 +45,7 @@
// 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 = {};
Meteor.accounts.oauth2._loginResultForState = {};
// Listen on /_oauth/*
__meteor_bootstrap__.app
@@ -40,14 +65,19 @@
// This way the subsequent call to the `login` method will be
// immediate.
var providerName = splitUrl[2];
var provider = Meteor.accounts.oauth2.providers[providerName];
var serviceName = splitUrl[2];
var service = Meteor.accounts.oauth2._services[serviceName];
// Get or create user id
var userId = provider.userIdForOauthReq(req);
var userInfo = service.handleOauthRequest(req.query);
var userId = Meteor.accounts.updateOrCreateUser(
userInfo.email, userInfo.userData, serviceName,
userInfo.serviceUserId, userInfo.serviceData);
// 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] =
Meteor.accounts.oauth2._loginResultForState[req.query.state] =
{token: loginToken, id: userId};
// We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request