Local Authorization
Have you ever seen a login page that lets you use Facebook or Google or Twitter or even LinkedIn? These are called social logins and they use OAuth authenticaton. Usually, you can use a username and password to login. This latter kind of login is called a local login because the system doesn't have to go to an external website to authenticate. Something like this:
Likely, the mechanism that handles that logging in part behind the scenes is the passport, an authentication middleware for Node.js written by the very prolific Jared Hanson.
One thing that often happens with this type of login is that for each type of authorization, a different user account is made for each type of login. So, if you login with Facebook today and set some user preferences, and you login with Google tomorrow, you won't have the same preferences because it's a different user account.
In the system I am going to develop, I am going to let the user create an account with either a local login using an email and password. I will assume that each of their social accounts uses the same email, so I will be able to link all the accounts together based on their email. If the accounts don't use the same email, they will wind up creating separate user accounts, but that's what often happens. It's likely that most users will only use one type of login.
Stick with me. I've got three more points to mention.
First, in the system that I'm developing, the backend does not have a view engine. That is, the database, or server side, can only serve JSON objects, data. It cannot serve front end pages. As I'll explain in a later chapter, this can cause CORS errors when doing social authentication. (What's CORS? Cross-Origin Resource Sharing. Explanation here.) I've got a way to get around that.
Second, I'm not going to use Twitter social login. Twitter does not need to have an email associated with it, so I can do my match-up trick.
Third, to get Facebook or Google or probably other social logins to work, the code must be on a server with a domain that you can give to Facebook or Google to authenticate. In this chapter, I'm just going to work on getting the local login to work. Believe me. That'll be enough.
Game Plan
Here's what we need to do:
- Make a login form
- Modify the user schema
- Install passport and write configuration code
- Write the routes and endpoints
- Fix up the Angular authService
Make a login form
Disclaimer This would probably be much easier if I knew how to use yo-angular. However, when I tried it, it didn't put stuff in the place I wanted it put. It seemed like it wanted to make a whole page and only one route and endpoint for each page, and I wanted something more flexible.
The register.html
and register.js
controller are going to be similar, so I'll just copy them and modify. So, I got a hold of the client/app/views/login.html
and modified it like this:
- Change the
<h3>
tag - search and replace 'register' for 'login'
- search and replace 'Register' for 'Login'
- Get rid of firstname, lastname, and one of the password inputs from the form
There. That was easy.
But, that login is going to need a controller. One of the things we changed in the html was a registerSubmit() to a loginSubmit(), so at least we have to have one of those.
It won't be quit as easy as the html file, but let's try:
- search and replace 'register' for 'login'
- search and replace 'Register' for 'Login'
I am going to take all the guts out $scope.loginSubmit() function and rewrite it.
Next, I can work on the dependencies. Before, the dependencies copied over from register.js looked like this:
angular.module('clientApp')
.controller('LoginCtrl', [
'$http',
'$scope',
function ($http, $scope) {
Most of the work is going to be done by the authService, so I have to inject the authService, which aliased to 'auth'. By taking all the insides out of $scope.loginSubmit, I got rid of all the $http calls in this controller, so I won't need that. I'm planning on calling an alert which comes from $window, so I'll need that. In the end, it looks like this:
angular.module('clientApp')
.controller('LoginCtrl', [
'auth',
'$scope',
'$window',
function (auth, $scope, $window) {
I like to put my dependencies in alphabetical order, ignoring any leading $. They have to be in the same order in the array members before the anonymous function and in the parameters for the function. More than once I've gotten it messed up, so I learned to alphabetize.
Inside the $scope.loginSubmit function, we only need a little bit of code. It's going to look like this:
$scope.loginSubmit = function () {
auth.logIn($scope.user);
};
I already made a goofy logIn() function that I call from the
These changes to client/app/views/top-nav.html
from this:
<li ng-hide="isLoggedIn()"><a href="" ng-click="logIn()">Log In</a></li>
to this:
<li ng-hide="isLoggedIn()"><a href="/#/login">Log In</a></li>
And clean up client/app/scripts/controllers/nav.js
. I'll move this to client/app/scripts/controllers/login.js
:
$scope.user = {
firstName: 'Bugs',
lastName: 'Bunny'
};
And I can get rid of this in nav.js
:
$scope.logIn = function () {
auth.logIn($scope.user);
};
All I have to do is add this new login view/controller thing to client/app/scripts/app.js
:
.state('login', {
url: '/login',
templateUrl: 'views/login.html',
controller: 'LoginCtrl',
controllerAs: 'login'
})
Let's give it a try. If I start the application
(mongod --dbpath data/db/ --logpath data/logs/mongodb.log --logappend
from the main root directory, grunt serve
from the client
directory, and npm start
from the server
directory and point the browser at http://localhost:3000/#/
), I can click on Log In
in the navigation, and get to my new form. Clicking on the green button will login as 'Bugs Bunny', the same as before.
I saved the code up to this point on the LoginForm
branch. You can get it as follows:
git clone -b LoginForm https://github.com/amnotafraid/MeanSeed.git
Modify the user schema
The user schema is going to need a place to put all the login credentials for social logins and the local login. Social logins, like facebook and google, will not need password authentication, so we need to take out the userSchema.pre('save',...
to write the password--cool as that functionality is. We will write different passport functionality. Each type of login may have a different first and last name, so we will have a firstname and lastname for every field.
The schema
In the server\app\database\schema\user.js
file, I am going to change the schema from this:
var userSchema = new Schema({
firstname: { type: String, required: true },
lastname: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
profile: {} // for extra information you may / may not want
});
to this:
var userSchema = new Schema({
email : {
type : String,
required : true,
unique : true
},
local : {
firstname : String,
lastname : String,
password : String,
salt : String
}
});
Mongoose will automatically give each user doc an _id. However, I am going to use email as a unique key, so this field is marked unique and required. Inside the user doc, I have a subdocument, local
to store all the credentials for local login.
setPassword and validPassword
Now, I have to write some code to encrypt a password and save it to the schema and to validate a password. To do this, I am going to need the crypto
module. From the server
directory, I install like this:
npm install crypto --save
Now, I can include that in the user schema:
var crypto = require('crypto');
I won't need bcrypt anymore, so I can remove this require:
var bcrypt = require('bcrypt');
I can remove the comparePassword and pre('save' ... methods, and replace them with my these two methods:
userSchema.methods.setPassword = function (password) {
this.local.salt = crypto.randomBytes (16).toString('hex');
this.local.password = crypto.pbkdf2Sync
(password,
this.local.salt,
1000,
64,
'sha1').toString('hex');
};
userSchema.methods.validPassword = function (password) {
var hash = crypto.pbkdf2Sync
(password,
this.local.salt,
1000,
64,
'sha1').toString('hex');
return this.local.password === hash;
};
That's all the changes to the user schema for now. Later on, I will circle back and jsonwebtoken I can send information back to the client when a user registers or logs in.
Install passport and write configuration code
To install passport, I can do this in the server
directory:
npm install passport passport-local --save
Now, I need to configure this so express can use it. In server\app\app.js
, I'll require it, right after path:
var passport = require('passport');
I will put the following configuration code in server\app\config\passport.js
(adding the new directory config
):
var db = require('../database');
var User = db.user;
module.exports = function (passport) {
passport.serializeUser (function (user, done) {
done (null, user.id);
});
passport.deserializeUser (function (id, done) {
User.findById (id, function (err, user) {
done (err, user);
});
});
require('./strategies/local.js')(passport);
};
The serializeUser and deserializeUser will access the user object and store it in the session.
I will add this code to server\app\config\strategies\local.js
(adding the new directory server\app\config\strategies
):
var LocalStrategy = require('passport-local').Strategy;
var db = require('../../database');
var User = db.user;
module.exports = function(passport) {
passport.use('local-login', new LocalStrategy({
// by default, local strategy uses username and password.
// Here, we override these field names
usernameField : 'email',
passwordField : 'password'
},
function(email, password, done) {
process.nextTick(function () {
User.findOne({ email: email}, function (err, user) {
if (err) {
return done(err);
}
if (!user) {
return done(null, false, { message: 'Incorrect email.' });
}
var fMatch = user.validPassword(password);
if (!fMatch) {
return done(null, false, { message: 'Incorrect password.' });
}
else {
return done(null, user);
}
});
});
})
);
};
I tell passport I want to use the email and password field of the request object, req
, and then I go check with the database to see if I can find that user.
There are three possible results:
- No user email
- Wrong password
- Correct login
Back in
server\app\app.js
, I have to call the configuration. I'll put these lines just afterapp.use(cookieParser())
:
It says call// passport config require('./config/passport')(passport); app.use(passport.initialize());
config/passport.js
and send it the require passport. Then, initialize passport in express middleware.
I saved the code up to this point on the ConfigLocal
branch. You can get it as follows:
git clone -b ConfigLocal https://github.com/amnotafraid/MeanSeed.git
Write the routes and endpoints
To the server/app/router/index.js
file, add a new route:
app.route('/login')
.post(user.login);
To server/app/router/endpoints/user.js
file, add the login method:
exports.login = function (req, res, next) {
if (!req.body.email || !req.body.password) {
return res.status(400).json ({
message: 'Please fill out email and password'
});
}
passport.authenticate('local-login', function(err, user, info) {
if (err) return next(err);
if (user) {
return res.status(200).json({
message:"Successful login"
});
} else {
return res.status(401).json(info);
}
})(req, res, next);
};
And add the passport require to the top:
var passport = require('passport');
I need to change the register method, too. I'll do it after I get the jsonwebtoken set up.
jsonwebtoken
A [JSON Web Token]{https://jwt.io/introduction/) is way to securly transmit information between the client and server. I am going to make a JSON web token that will let the server tell the client some information about the user who is logged in. First of all, in the server
directory, install jsonwebtoken:
npm install jsonwebtoken --save
And on the user schema in server/app/database/schema/user.js
file, add the require:
var jwt = require('jsonwebtoken');
and the method to create the token:
userSchema.methods.generateJWT = function () {
// set expiration to 1 day
var today = new Date();
var exp = new Date(today);
exp.setDate (today.getDate() + 1);
var obj = {
_id: this._id,
firstname: this.local.firstname,
lastname: this.local.lastname,
exp: parseInt(exp.getTime() / 1000)
};
return jwt.sign (obj,
'secret');
};
This is going to pack up the user._id, firstname and lastname into a jsonwebtoken that can be sent back to the client. I am going to do that here in server/app/router/endpoints/user.js
:
if (user) {
return res.json({token: user.generateJWT()});
} else {
return res.status(401).json(info);
}
Now that I'm using jsonwebtoken, and I'm no longer automatically setting a password when I save the user, I have to modify the register
method, too.
Before:
exports.register = function (req, res, next) {
console.log(JSON.stringify(req.body, null, 2));
// Check to see if the user already exists
// using their email address
User.findOne({
'email': req.body.email
}, function (err, user) {
// If there's an error, log it and return to user
if (err) {
console.log('Couldn\'t create new user because of: ' + err);
// send the error
res.status(500).json({
'message': 'Internal server error from signing up new user.'
});
}
// If the user doesn't exist, create one
if (!user) {
// setup the new user
var newUser = new User({
firstname: req.body.firstname,
lastname: req.body.lastname,
email: req.body.email,
password: req.body.password1
});
// save the user to the database
newUser.save(function (err, savedUser, numberAffected) {
if (err) {
console.log('Problem saving the user due to ' + err);
res.status(500).json({
'message': 'Database error trying to sign up.'
});
}
res.status(201).json({
'message': 'Successfully created new user'
});
});
}
// If the user already exists...
if (user) {
res.status(409).json({
'message': req.body.email + ' already exists!'
});
}
});
};
to this:
exports.register = function (req, res, next) {
if (!req.body.email || !req.body.password) {
return res.status(400).json({
message: 'Please fill out all fields'
});
}
// Check to see if the user already exists
// using their email address
User.findOne({
'email': req.body.email
}, function (err, user) {
// If there's an error, log it and return to user
if (err) return next(err);
// If the user doesn't exist, create one
if (!user) {
// setup the new user
var newUser = new User({
email: req.body.email,
local: {
firstname: req.body.local.firstname,
lastname: req.body.local.lastname
}
});
newUser.setPassword(req.body.password);
// save the user to the database
newUser.save(function (err, savedUser, numberAffected) {
if (err) return next(err);
var returnObj = {};
returnObj.token = savedUser.generateJWT();
res.status(200).json({token: savedUser.generateJWT()});
});
}
// If the user already exists...
if (user) {
res.status(409).json({
'message': req.body.email + ' already exists!'
});
}
});
};
Start using dedicated database
Up until now, I have been using the default test database. I am going to change this in server/app/database/index.js
by changing from this:
var developmentDb = 'mongodb://localhost/test';
var productionDb = 'mongodb://localhost/test';
to this:
var developmentDb = 'mongodb://localhost/mean';
var productionDb = 'mongodb://localhost/mean';
I saved the code up to this point on the ConfigLocal2
branch. You can get it as follows:
git clone -b ConfigLocal2 https://github.com/amnotafraid/MeanSeed.git
Fix up the Angular authService
Back on the front end, the client side, I need to fix it up so that the register and login forms and controllers so that they will connnect up with the server side correctly.
First of all, I want to make sure that the user register model corresponds to how I have it defined in the user schema. I put the firstname and lastname in the local subdocument by changing from ng-model="user.firstname"
to ng-model="user.local.lastname"
and from ng-model="user.lastname"
to ng-model="user.local.lastname"
and I'll quit with password1 and just call it password. From this:
<label class="col-md-4 control-label" for="password1">Password</label>
<div class="col-md-5">
<input id="password1" name="password1" ng-model="user.password1" type="password" placeholder="" class="form-control input-md">
to this:
<label class="col-md-4 control-label" for="password">Password</label>
<div class="col-md-5">
<input id="password" name="password" ng-model="user.password" type="password" placeholder="" class="form-control input-md">
authService.js
The client/app/scripts/services/authService.js
file is going to need some work. I am going to inject $http, $state, and $window. So, from this:
angular.module('clientApp')
.factory('auth', [
function () {
return new Auth();
}
]);
to this:
angular.module('clientApp')
.factory('auth', [
'$http',
'$state',
'$window',
function ($http, $state, $window) {
return new Auth($http, $state, $window);
}
]);
And from this:
function Auth() {
to this:
function Auth($http, $state, $window) {
To log in, I need to post to that new route I made, so I'll swap out the logIn function like this:
this.logIn = function(user){
return $http.post('/login', user)
.success(function(data){
_this.saveToken(data.token);
});
};
See that saveToken function in there? The login route is going to return that jsonwebtoken, and I need to put it somewhere, so I'll add this function:
this.saveToken = function (token) {
$window.localStorage['mean-token'] = token;
};
So, all I'm doing is storing that token in HTML5 localStorage. To read the token, I have to parse it, so I will make this getToken function:
this.getToken = function () {
return $window.atob(token.split('.')[1]);
};
Now, to fix up the currentUser
method, I'm going to use the token to return the first and last name, like this:
this.currentUser = function () {
if (_this.isLoggedIn ()) {
var token = _this.getToken ();
var payload = JSON.parse ($window.atob (token.split('.')[1]));
return payload.firstname + ' ' + payload.lastname;
}
};
I need to change the isLoggedIn method so that it will check and make sure that the token hasn't expired. Like this:
this.isLoggedIn = function () {
var token = _this.getToken();
if (token) {
var payload = JSON.parse($window.atob(token.split('.')[1]));
return payload.exp > Date.now() / 1000;
}
else {
return false;
}
};
To logout, all I have to do is delete the token from localStorage and put the user back on the home page, like this:
this.logOut = function () {
$window.localStorage.removeItem('mean-token');
$state.go('home');
};
And I am going to add a register method this will post the stuff from the register form to the '/register' route:
this.register = function (user) {
return $http.post ('/register', user)
.success (function (data) {
_this.saveToken (data.token);
})
.error (function (data, status) {
_this.error = data.message;
});
};
Note: When I tried to register a user from the form, I kept getting this message in the console of the web browser:
angular.js:14324 TypeError: $http.post(...).success is not a function
at Auth.register (authService.js:45)
at ChildScope.$scope.registerSubmit (register.js:35)
What happened? While I was coding this up, the most recent version of angular changed. This code worked in angular version 1.5.9, but failed in version 1.6.0. I was getting the angular by executing bower install
in the client
directory. This would get a version greater than 1.4.0. One time version 1.5.9. A few weeks later version 1.6.0. Here it says:
$http's deprecated custom callback methods - success() and error() - have been removed. You can use the standard then()/catch() promise methods instead, but note that the method signatures and return values are different.
Instead of fixing the code I wrote, I went back and got an old version of angular (my bad). In client\bower.json
, I changed from this:
"angular": "^1.4.0",
"bootstrap-sass-official": "^3.2.0",
"angular-animate": "^1.4.0",
"angular-aria": "^1.4.0",
"angular-cookies": "^1.4.0",
"angular-messages": "^1.4.0",
"angular-resource": "^1.4.0",
"angular-route": "^1.4.0",
"angular-sanitize": "^1.4.0",
"angular-touch": "^1.4.0",
"angular-ui-router": "^0.3.1"
},
"devDependencies": {
"angular-mocks": "^1.4.0"
to this:
"angular": "~1.5",
"bootstrap-sass-official": "^3.2.0",
"angular-animate": "~1.5",
"angular-aria": "~1.5",
"angular-cookies": "~1.5",
"angular-messages": "~1.5",
"angular-resource": "~1.5",
"angular-route": "~1.5",
"angular-sanitize": "~1.5",
"angular-touch": "~1.5",
"angular-ui-router": "^0.3.1"
},
"devDependencies": {
"angular-mocks": "~1.5"
And since the user is getting saved in the database where it belongs and there is a way to store the jsonwebtoken in local storage, I don't need these guys anymore:
this.fLoggedIn = false;
this.user = {};
Change register controller
I need to change client/app/scripts/controllers/register.js
to match up with the changes on the form and to use the auth service correctly.
Change from password1
to password
, and put the firstname
and lastname
in a local subdocument. I want to use alerts, so I'll inject $window
and make sure any alerts are called as $window.alert(...
Now that I have moved the firstname and lastname to local
subdocument in the user object, I need to check that when I validate the form. When I get done registering, I want to send the user to the home page, so I'll inject $state, so I can do a $state.go(...
. Finally, I need to call the register method on the auth service. In the end, the whole thing looks like this:
'use strict';
angular.module('clientApp')
.controller('RegisterCtrl', [
'auth',
'$scope',
'$state',
'$window',
function (auth, $scope, $state, $window) {
$scope.user = {};
// This is our method that will post to our server.
$scope.registerSubmit = function () {
// make sure all fields are filled out...
if (
!$scope.user.local ||
!$scope.user.local.firstname ||
!$scope.user.local.lastname ||
!$scope.user.email ||
!$scope.user.password ||
!$scope.user.password2
) {
$window.alert('Please fill out all form fields.');
return false;
}
// make sure the passwords match match
if ($scope.user.password !== $scope.user.password2) {
$window.alert('Your passwords must match.');
return false;
}
auth.register($scope.user)
.error (function (error) {
$window.alert(error.message);
})
.then (function () {
$state.go('home');
});
};
}]);
Change login controller
I want to change the login controller, client/app/scripts/controllers/login.js
, so that it will send the login form information to the auth service. If it's successful it should send the user to the home page. If not, it should show an alert. The whole thing ended up looking like this:
'use strict';
angular.module('clientApp') // make sure this is set to whatever it is in your client/scripts/app.js
.controller('LoginCtrl', [
'auth',
'$scope',
'$state',
'$window',
function (auth, $scope, $state, $window) {
$scope.user = {};
// This is our method that will post to our server.
$scope.logIn = function() {
auth.logIn($scope.user).error(function(error){
$window.alert(error.message);
}).then(function(){
$state.go('home');
});
};
}]);
I saved the code up until this point on the MeanLocalAuth branch. You can get it like this:
git clone -b MeanLocalAuth https://github.com/amnotafraid/MeanSeed.git