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:
+2
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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
@@ -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')
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user