fix(facebook): move from passport-facebook to Facebook JS SDK. Better

security on FB login by validating accessToken. Create user from FB
profile if none exists, allows 3rd-party apps to. Fixes #4221
This commit is contained in:
Tyler Renelle
2014-11-05 16:31:52 -07:00
parent 8ff971b1ec
commit 8651f0da25
8 changed files with 78 additions and 111 deletions
+2 -1
View File
@@ -40,7 +40,8 @@
"ngInfiniteScroll": "1.0.0",
"jquery-colorbox": "~1.4.36",
"pnotify": "~1.3.1",
"jquery-ui": "~1.10.3"
"jquery-ui": "~1.10.3",
"angular-facebook": "~0.2.3"
},
"devDependencies": {
"angular-mocks": "1.3.0-beta.11"
+26 -22
View File
@@ -5,26 +5,15 @@
*/
angular.module('authCtrl', [])
.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal',
function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal) {
var runAuth;
var showedFacebookMessage;
$scope.useUUID = false;
$scope.toggleUUID = function() {
if (showedFacebookMessage === false) {
alert(window.env.t('untilNoFace'));
showedFacebookMessage = true;
}
$scope.useUUID = !$scope.useUUID;
};
.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal', 'Facebook',
function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal, Facebook) {
$scope.logout = function() {
localStorage.clear();
window.location.href = '/logout';
};
runAuth = function(id, token) {
var runAuth = function(id, token) {
User.authenticate(id, token, function(err) {
$window.location.href = '/';
});
@@ -59,14 +48,10 @@ angular.module('authCtrl', [])
username: $scope.loginUsername || $('#login-tab input[name="username"]').val(),
password: $scope.loginPassword || $('#login-tab input[name="password"]').val()
};
if ($scope.useUUID) {
runAuth($scope.loginUsername, $scope.loginPassword);
} else {
$http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
}
$http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
};
$scope.playButtonClick = function(){
@@ -126,5 +111,24 @@ angular.module('authCtrl', [])
$scope.hasNoNotifications = function() {
return selectNotificationValue(false, false, false, false, true);
}
// ------ Facebook ----------
// See https://developers.facebook.com/docs/facebook-login/login-flow-for-web/v2.2 for boilerplate
$scope.fbLogin = function(){
var thenLogin = function(response){
$http.post(ApiUrlService.get() + "/api/v2/user/auth/facebook", response.authResponse)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
}
Facebook.getLoginStatus(function(response) {
if (response.status === 'connected') {
thenLogin(response);
} else {
Facebook.login(thenLogin)
}
});
}
}
]);
+7 -2
View File
@@ -1,18 +1,23 @@
"use strict";
window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap'])
window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices', 'userServices', 'chieffancypants.loadingBar', 'authCtrl', 'ui.bootstrap', 'facebook'])
.constant("API_URL", "")
.constant("STORAGE_USER_ID", 'habitrpg-user')
.constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings')
.constant("MOBILE_APP", false)
habitrpgStatic.controller("PlansCtrl", ['$rootScope',
.config(['FacebookProvider', function(FacebookProvider){
FacebookProvider.init(window.env.FACEBOOK_KEY);
}])
.controller("PlansCtrl", ['$rootScope',
function($rootScope) {
$rootScope.clickContact = function(){
window.ga && ga('send', 'event', 'button', 'click', 'Contact Us (Plans)');
}
}
])
.controller('AboutCtrl',[function(){
$(document).ready(function(){
$('a.gallery').colorbox({
+1
View File
@@ -83,6 +83,7 @@
"bower_components/angular-bootstrap/ui-bootstrap-tpls.js",
"bower_components/bootstrap/dist/js/bootstrap.js",
"bower_components/jquery-colorbox/jquery.colorbox-min.js",
"bower_components/angular-facebook/lib/angular-facebook.js",
"bower_components/angular-loading-bar/build/loading-bar.js",
"js/env.js",
+35 -76
View File
@@ -168,22 +168,43 @@ api.loginLocal = function(req, res, next) {
/*
POST /user/auth/facebook
*/
api.loginFacebook = function(req, res, next) {
var facebook_id = req.body.facebook_id;
if (!facebook_id) return res.json(401, {err: 'No facebook id provided'});
User.findOne({'auth.facebook.id': facebook_id}, function(err, user) {
if (err) {
return res.json(401, {err: err});
} else if (user) {
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
return res.json(200, {id: user.id,token: user.apiToken});
} else {
/* FIXME: create a new user instead*/
return res.json(403, {err: "Please register with Facebook on https://habitrpg.com, then come back here and log in."});
var accessToken = req.body.accessToken;
async.waterfall([
function(cb){
// TODO is this private function here safe to use?
passport._strategies.facebook.userProfile(accessToken, cb);
},
function(profile, cb) {
User.findOne({'auth.facebook.id': profile.id}, {_id:1, apiToken:1, auth:1}, function(err, user){
if (err) return cb(err);
cb(null, {user:user, profile:profile});
});
},
function(data, cb){
if (data.user) return cb(null, data.user);
// Create new user
var prof = data.profile;
var user = new User({
preferences: {
language: req.language // User language detected from browser, not saved
},
auth: {
facebook: prof,
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
});
user.save(cb);
if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){
emailUser((prof.displayName || prof.username), prof.emails[0].value, 'welcome');
}
ga.event('register', 'Facebook').send();
}
});
], function(err, user){
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
return res.json(200, {id: user.id, token:user.apiToken});
})
};
api.resetPassword = function(req, res, next){
@@ -271,66 +292,4 @@ api.setupPassport = function(router) {
res.redirect('/');
})
// GET /auth/facebook
// Use passport.authenticate() as route middleware to authenticate the
// request. The first step in Facebook authentication will involve
// redirecting the user to facebook.com. After authorization, Facebook will
// redirect the user back to this application at /auth/facebook/callback
router.get('/auth/facebook',
passport.authenticate('facebook', {scope: 'email'}),
i18n.getUserLanguage,
function(req, res){
// The request will be redirected to Facebook for authentication, so this
// function will not be called.
});
// GET /auth/facebook/callback
// Use passport.authenticate() as route middleware to authenticate the
// request. If authentication fails, the user will be redirected back to the
// login page. Otherwise, the primary route function function will be called,
// which, in this example, will redirect the user to the home page.
router.get('/auth/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/login' }),
i18n.getUserLanguage,
function(req, res) {
//res.redirect('/');
async.waterfall([
function(cb){
User.findOne({'auth.facebook.id':req.user.id}, cb)
},
function(user, cb){
if (user) return cb(null, user);
user = new User({
preferences: {
language: req.language // User language detected from browser, not saved
},
auth: {
facebook: req.user,
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
});
user.save(cb);
if(isProd && req.user.emails && req.user.emails[0] && req.user.emails[0].value){
emailUser((req.user.displayName || req.user.username), req.user.emails[0].value, 'welcome');
}
ga.event('register', 'Facebook').send()
}
], function(err, saved){
if (err) return res.redirect('/static/front?err=' + err);
req.session.userId = saved._id;
res.redirect('/static/front?_id='+saved._id+'&apiToken='+saved.apiToken);
})
});
// Simple route middleware to ensure user is authenticated.
// Use this route middleware on any resource that needs to be protected. If
// the request is authenticated (typically via a persistent login session),
// the request will proceed. Otherwise, the user will be redirected to the
// login page.
// function ensureAuthenticated(req, res, next) {
// if (req.isAuthenticated()) { return next(); }
// res.redirect('/login')
// }
};
+1
View File
@@ -192,6 +192,7 @@ module.exports.locals = function(req, res, next) {
siteVersion: siteVersion,
Content: shared.content,
mods: require('./models/user').mods,
FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'),
tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
+4 -9
View File
@@ -73,20 +73,15 @@ if (cluster.isMaster && (isDev || isProd)) {
done(null, obj);
});
// Use the FacebookStrategy within Passport.
// Strategies in Passport require a `verify` function, which accept
// credentials (in this case, an accessToken, refreshToken, and Facebook
// profile), and invoke a callback with a user object.
// FIXME
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
passport.use(new FacebookStrategy({
clientID: nconf.get("FACEBOOK_KEY"),
clientSecret: nconf.get("FACEBOOK_SECRET"),
callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
},
function(accessToken, refreshToken, profile, done) {
// To keep the example simple, the user's Facebook profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the Facebook account with a user record in your database,
// and return that user instead.
done(null, profile);
}
));
+2 -1
View File
@@ -3,8 +3,9 @@ script(id='modals/login.html', type='text/ng-template')
button.close(type='button', ng-click='$close()') ×
h4.modal-title=env.t('loginAndReg')
.modal-body(ng-controller='AuthCtrl')
a(href='/auth/facebook')
a(href='#', ng-click='fbLogin()')
img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt=env.t('loginFacebookAlt'))
//can we add in google auth? I like google auth
h3=env.t('or')
ul.nav.nav-tabs