In this tutorial, I will be showing you guys how to create a basic web app from scratch that would implement authentication on Node.js.
You can see how the node app will look like upon finishing this tutorial. This app will give you a nice detailed insight on how you database, session, webpages routers, and security works in node app. So lets get started.
Prerequisites : Here, I would assume that you are already familiar with basics of Node.js , MongoDb and Express. If you aren't familiar with it, i would recommend you to follow this tutorial http://apprajapati94.blogspot.ca/2016/06/get-up-and-running-with-nodejs-mongodb.html , its most basic tutorial to get started with Node and Express.
In any platform of Internet, Authentication is the most crucial and important this in every phase. Authentication is the only way to make the internet secure place and fundamental to internet security. According to me, every programmer should be aware of how authentication works.
Also, I would want to have your mongoDB and Node.js installed on your machine. So, lets get started,
Also, I would want to have your mongoDB and Node.js installed on your machine. So, lets get started,
Prepare the basic App structure
In this step, we are going to create a basic app structure.
First of all, create an project directory, in this case, i am naming it authentication and create following app structure with views director and app.js file.
As you can see, I have authentication directory which will act us our main project directory. In that, i have views directory and app.js file .
Now, inside views directory, we want to put our webpages code. views means what will be viewed on web page. We are going to use jade as rendering engine. So, lets create following files inside views directory.
Now, lets add some code to this files...
inside base.jade file, add following
block vars
doctype html
html
head
title Auth | #{title}
link(rel='stylesheet', href='/stylesheets/style.css')
body
block body
div#contact apprajapati9.blogspot.ca | Blog Of Programmer
This will create a base file for our jade files. The purpose of this file, is that we don't need to write the same block of html to body part of code in every jade file. You can directly extends this and start adding your body to jade file. In the next file, we will just need to extends this file, so we will not have to write html to body part in every file.now inside dashboard.jade add following
extends base
block vars
- var title = 'Dashboard'
block body
h1 Dashboard
p.
Welcome to your dashboard! You are logged In.
p.
<a href="https://www.blogger.com/logout">LogOut! </a>
As you can see, we just wrote extends base to get the code from base.jade file. This file will serve as our dashboard and will be seen when user is logged in.Now, lets add following code inside index.jade file. This file will serve as our home page.
extends base
block vars
- var title = 'Home'
block body
h1 Authentication Web App!
#container.col
br
br
p.
Welcome to home page! Please <a href="https://www.blogger.com/login">Login</a> or <a href="https://www.blogger.com/register"> Register </a> to continue.
Now, we want to have our login page, so user can login. So, to accomplish that, add following code in your login.jade file,
extends base
block vars
- var title = 'Login'
block body
h1 Log Into Your Account
#container.col
p
br
if error
p ERROR: #{error}
form(method="post")
input(type="hidden", name="_csrf", value=csrfToken)
span Email :
input(type="email", name="email",required=true)
br
span Password :
input(type="password", name="password", required=true)
br
input(type="submit")
This will create a basic login page with Email and password input fields.Now, lets add register.jade which would allow user to register as new user.
extends base
block vars
- var title = 'Register'
block body
h1 Create an Account
#container.col
br
br
form(method="post")
input(type="hidden", name="_csrf", value=csrfToken)
span First Name :
input(type="text", name="firstName", required=true)
br
span Last Name :
input(type="text", name="lastName", required=true)
br
span Email Address :
input(type="email", name="email", required=true)
br
span Password :
input(type="password", name="password", required=true)
br
input(type="submit")
at this point, we have all pages ready. To enable css , i have already added link of css file in base.jade file. You need to create a directory in your main project directory (authentication) called public. In this directory, we will put all the styling of css. ( headers.txt is not necessary )
now inside public directory create a folder named stylesheets where we can put our style.css file.
add following code in your style.css file. We need to create stylesheets folder, because we specified path in base.jade file link(rel='stylesheet', href='/stylesheets/style.css') which means, we must put into correct location, so it can find the file.
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
background-repeat: no-repeat;
background-position: 5% 0%;
text-align: center;
}
a {
color: #00B7FF;
}
.col{
border: 1px solid #00B7FF;
}
#contact{
text-decoration: underline;
text-align: right;
margin-top: 100px;
}
Now lets add basic routes and create an express app.
add following in your app.js file.
//including express package.
var express = require ('express')
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine','jade');
// Let express know there's a public directory.
app.use(express.static('./public'));
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
//finally listen on port 3000
app.listen(3000);
In app.js file, we are basically doing following things
- get the express instance
- tell the express app to set jade as view engine
- telling express to use public directory for static elements such as js, css, images for jade.
- get the route "/" - homepage and tell what page needs to be rendered
- set the port to run.
Now that we have app.js ready, we can say , we need two packages - express and jade. This two packages we have included. This creates a basic server, which will open index.jade (read comments for understanding code)
Now, first of all install the packages by following commands on command line
Cmd :
> npm install express
> npm install jade
// you can install both together with following command
> npm install express jade
// now lets run the app
> node app.js
Now, our server is running on port 3000. Lets go to browser and test it out index.jade page. type in localhost:3000 in your browser .
This is localhost:3000/ - this triggers following code in the app
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
and it tells the express to render the index.jade file.So, in similar way, we can tell add other routes. add following code in your app.js file , so we can get all the pages working. Read comments for understanding the code.
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
//including express package.
var express = require ('express')
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine','jade');
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function(req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function(req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function(req, res) {
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout',function(req, res){
res.redirect('/');
});
//finally listen on port 3000
app.listen(3000);
So this will enable us to render to other pages through url as follows
localhost:3000/login - login.jade
localhost:3000/register -- register.jade
Http Request extraction in Authentication
Now that we have webserver with working routes, lets dive into authentication steps. Be patient, there is lot go on yet!
The first real question is - Where authentication starts in the browser! When you enter your information on login page or register page, that's where it starts to authenticate.
Lets try to see the source code of localhost:3000/login , right click on the web page and click on view page source (Ctrl+U). you will see the following,
Now above code is minified. It basically means, it removes the unnecessary code or spaces from the code. In order to see the cleaner code, add following in app.js file
// add this after setting up the view engine as jade
//
app.locals.pretty = true;
This code will enable us to non minified code. After adding the above code, lets run your web server with node app.js command
Now that our server is running, lets go to localhost:3000/login and view the source code.
Look at the picture below, it shows the non minified source code of our login.jade file into html, because browser renders html only.
When you will open the source code of our app, you can see post , this is where all the magic happens.
<form method="post">
<input name="_csrf" type="hidden" />Email :
<input name="email" required="" type="email" /><br />
Password :
<input name="password" required="" type="password" /><br />
<input type="submit" />
</form>
This is where the authentication starts. When you pass form with method post , it means that whatever you entered in the fields of form, when you press submit, it will send the data to http request and send it over the webserver.Above picture demonstrates that when you press submit button of form with post method, it will send request.
Lets look at what is request actually contains!! To see what is being passed through POST request, we can use body-parser package.
body-parser documentation - > https://www.npmjs.com/package/body-parser
Body parser is node.js middleware, and it parses the incoming request bodies.
So, lets implement this middleware in our app. Install this module into our app
This installs the body-parser in our app.
Now to initialize this in our app, add following in our app.js file
var bodyParser = require('body-parser');
now add following with POST . This middleware is going to take the body of HTTP request that user is making to webserver and allow us to access that body.//adding middleware
//It returns the middleware of bodyParser that parses Urlencoded bodies.
// generally url is encoded, it parses its body.
app.use(bodyParser.urlencoder({extended:true});
Now, to see what's being passed through register or login page, lets add following code
app.post('/register',function(req, res){
res.json(req.body);
// It means - send us the data back that is passed by the user
});
The middleware makes the body of http request available via req.body , which we have used to render in form of JSON.
In the end, your app.js file should look like as follows :
var bodyParser = require('body-parser');
//including express package.
var express = require ('express');
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine','jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({extended:true}));
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function(req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function(req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function(req, res) {
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout',function(req, res){
res.redirect('/');
});
// ----------> ALL POST REQUESTS ----------------//
app.post('/register', function(req, res){
res.json(req.body);
})
//finally listen on port 3000
app.listen(3000);
Now, lets run this, and see what we are passing through post request.
Enter the data
Press Submit, you will get following response in JSON.
This is exactly what user is sending to us. So, now i hope, it would make it clear that why should we use this middleware. It allows us to access the http request body. This is considered to be the first step to start the authentication in node.js.
Now that we know how to fetch the data from html form, lets move on to next step.
MongoDB Setup
In this step, we are going to store the data which is passed by the user in MongoDb.
I assume that you have already MongoDb is installed and running on your machine. Use the command line and fire up your MongoDB.
In order to connect to MongoDb, we need to use mongoose library. It's popular library, and it handles the MongoDb object to connect asynchronously to our database.
Install mongoose in our project directory :
CMD :
> npm install mongoose
It will install the mongoose in our node_modules directory. Now that we have mongoose, lets implement in our app.js for login and registration.
Initialize Mongoose!
var mongoose = require('mongoose');
We can establish the connection to our app by following code
mongoose.connect('mongodb://localhost/auth');
This will allow us to connect to our database. Remember, you must have your mongodb running on shell. This will connect to your database and use auth database in your mongodb, and if it doesn't exist, it will create one by itself.How do we store user information in database? '
In order to store the the information in mongodb, we must create a Schema of our database.
It defines what fields, data type and collection we are going to use from MongoDB. You can define/import schema as follows :
var schema = mongoose.Schema;
var objectId = schema.ObjectId;
Now, we can define our fields and collection name as follows// 'User' -- collection name
// id, firstName, lastName, email, password = All fields are going to be in User collection.
var User = mongoose.model('User', new Schema({
id : objectId,
firstName : String,
lastName : String,
email : {type:String, unique:true},
password: String,
}));
This defines our collection and data fields inside collection is going to be. At this point after adding all above code, your app.js file should look like as follows :var bodyParser = require('body-parser');
var mongoose = require('mongoose');
//including express package.
var express = require ('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
// define collection fields
var User = mongoose.model('User', new Schema({
id : objectId,
firstName : String,
lastName : String,
email : {type: String, unique : true},
password : String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine','jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({extended:true}));
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function(req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function(req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function(req, res) {
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout',function(req, res){
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function(req, res){
res.json(req.body);
})
//finally listen on port 3000
app.listen(3000);
Now, we have mongodb database and collection schema set up. Now, lets store the information that user enters on register page.
You can achieve that by following :
app.post('/register', function(req, res){
// res.json(req.body); to get JSON form of data from body parser middleware
//getting the user's information from body parser
// body-parser middleware allow us to get the information passed in form post method.
// here we are simply getting it from req.body and creating a MongoDb object of fields, so we can store
var currentUser = new User({
firstName : req.body.firstName,
lastName :req.body.lastName,
email : req.body.email,
password : req.body.password,
});
// Now currentUser has all data, that user entered on Register.jade file
// we are saving it inside database using
// currentUser.save()
currentUser.save(function(err){
// if error causes - Try again,
// 11000 code specifically for unique - when duplication happens it throws error code 11000
// we have defined email as unique in Schema
if(err){
var error = 'Something went Wrong! Try again';
if(err.code === 11000){
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error : error});
}
else{
res.redirect('/dashboard');
}
});
});
Read the comments to understand the code. Now, lets test it out. Go to your localhost:3000 and enter the information on register page.After successfully redirected to dashboard, you can verify your mongodb
As you can see in the above image, my record has been successfully added into the database. Other fields such as _id and __v is added by mongodb and mongoose itself, so we are not going to worry about that.
Now, lets enable ourselves to login using the email and password stored in database. For that, add the following code in your app.js
app.post('/login',function(req, res){
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({email : req.body.email}, function(err, user){
//if user doesn't exist then Invalid Error
if(!user){
res.render('login.jade', {error : 'Invalid Email Address or Password'});
}
//if user exists check for password
else{
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if(req.body.password == user.password){
res.redirect('dashboard');
}
//if password doesn't match, redirect to loing page!
else{
res.render('login.jade', {error : 'Invalid Email Address or Password'});
}
}
});
});
read the comments to understand the code. The above code will validate the email and password entered by the user. It will let the user redirect to dashboard, if user exists in mongodb, if not, it will give us an error message.I know this is bit complex, but if you read carefully, and also read my comments, it will be super easy to understand.
At this point, your app.js should look like this and it should be functioning properly. It would enable us to login and register using mongodb data. Your app.js should look like this.
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
//including express package.
var express = require ('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
var User = mongoose.model('User', new Schema({
id : objectId,
firstName : String,
lastName : String,
email : {type: String, unique : true},
password : String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine','jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({extended:true}));
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function(req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function(req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function(req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function(req, res) {
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout',function(req, res){
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function(req, res){
// res.json(req.body); to get JSON form of data from body parser middleware
//getting the user's information from body parser
var currentUser = new User({
firstName : req.body.firstName,
lastName :req.body.lastName,
email : req.body.email,
password : req.body.password,
});
currentUser.save(function(err){
if(err){
var error = 'Something went Wrong! Try again';
if(err.code === 11000){
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error : error});
}
else{
res.redirect('/dashboard');
}
});
});
app.post('/login',function(req, res){
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({email : req.body.email}, function(err, user){
//if user doesn't exist then Invalid Error
if(!user){
res.render('login.jade', {error : 'Invalid Email Address or Password'});
}
//if user exists check for password
else{
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if(req.body.password == user.password){
res.redirect('dashboard');
}
//if password doesn't match, redirect to loing page!
else{
res.render('login.jade', {error : 'Invalid Email Address or Password'});
}
}
});
});
//finally listen on port 3000
app.listen(3000);
Sessions in Node.js
The idea of session is pretty cool. Typically, When user logs in, you redirect them to dashboard or home page. In this case, user is able to log in because user sent us the authentication detail through html post request, so you have that information on server and server knows who they are! But, once the user is logged in, and when user visits another pages available in web app they are not sending you the details of authentication explicitly. Then question is that how do you get that information when you redirect to other page after logging in? How do you remember that this person has logged in before and that person is visiting this page?
And the way that works is using sessions. It is an abstract term, but the mechanism of sessions work using cookies.
So, the general idea is this! When user logs in, you store that information in cookies, and that saved cookies will be used to retrieve the user details and identify the person logged in.
What's the browser cookie?
Imagine a child munching a cookie, leaving a trail of crumbs to follow. A computer browser cookie works the same way, leaving crumbs of identifying data. Cookies only contain plain text data and it cannot have executable code inside. The plain text in cookie can contain user ID, session ID, or any other text that represents data. Also, cookies can be read by only unique website which places it in your browser. If you clear your cookies, you will be logged out from every website in your browser, because when cookie isn't set up in browser, browser thinks of you as new visitor/visitor.
Cookies are crucial for login mechanism, without it, you wouldn't be able to log into websites. As i explained before, when you log in, that's fine! because you are providing the information, but when you redirect to other pages of website after login, it needs cookies in order to continue your session.
Implement session cookie in web authentication app
Now, to implement in our web app, we are going to use client-sessions library developed by mozilla. Once again, you can use other libraries, but i choose this one because it has great documentation, and its best one available in terms of security options available in it.
lets install it in our app using command line
npm install client-sessions
Now that we have library installed in our project directory, lets add some code to enable sessions!
First initialize it in our node app.
var sessions = require('client-sessions');
Now that we have client-sessions library's object sessions, we can set up the cookie information for our web app as follows ://place it after the express() app initialization .
app.use(sessions({
//name of cookie to use in our web application
cookieName : 'session',
//this string will be used by the application to
// encrypt or decrypt the information stored in cookie
secret : 'iw090909jo32032re2e2e2e32e23ee23',
//amount of milliseconds ! expiry duration
duration : 30*60*1000,
//min active duration.
activeDuration : 5*6*1000,
}));
Now, lets create a session when logged in. In order to do that, when you log in, and if user is valid, then you want to store that particular verified user into sessions. You can do that by adding following line in your post('/login') route.
// we just need to add this line in order to store the logged in user to our session cookie
req.session.user = user;
So, at this point, your login route will be as follows. The basic idea is to when user is verified, you just need to add that verified user object into your session.app.post('/login', function (req, res) {
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({ email: req.body.email }, function (err, user) {
//if user doesn't exist then Invalid Error
if (!user) {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
//if user exists check for password
else {
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if (req.body.password == user.password) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('dashboard');
}
//if password doesn't match, redirect to loing page!
else {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
}
});
});
Cool! Now, you have user saved in session!So, now lets render the details of user on dashboard using information stored in sessions.
When the user logs in, they will be redirected to dashboard. lets add following to get('/dashboard') route.
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function (req, res) {
if(req.session && req.session.user){
User.findOne({email : req.session.user.email}, function(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
}
else{
res.locals.user = user;
res.render('dashboard.jade');
}
});
}else{
res.redirect('/login');
}
});
Basically, here we are just validating that if session.user exists, it means that user has logged in already. In that case, just compare it with database! If true, then it means, user is completely verified.To retrieve the value and render it on our web page, We can use
res.locals.user = user ;
what this does is, it makes the user variable available throughout the app and template. So, using jade, we can get the details from the locally set up variable called user.
We need to change our dashboard.jade file in order to render the details.
add the following code inside dashboard.jade
extends base
block vars
- var title = 'Dashboard'
block body
h1 Dashboard
p.
Welcome to your dashboard! You are logged In.
#container.col
h2 User Information
p First Name : #{user.firstName}
p Last Name : #{user.lastName}
p Email : #{user.email}
p.
<a href="\logout">LogOut! </a>
In this file,#{user.firstName} - this statement will get the value from local object set up using
res.locals.user = user;
And all other variable will get the value from user object, because user object contains all the data stored in database.
At this point, your app.js file should be looking like this!
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var sessions = require('client-sessions');
//including express package.
var express = require('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
var User = mongoose.model('User', new Schema({
id: objectId,
firstName: String,
lastName: String,
email: { type: String, unique: true },
password: String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine', 'jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(sessions({
//name of cookie to use in our web application
cookieName : 'session',
//this string will be used by the application to
// encrypt or decrypt the information stored in cookie
secret : 'iw090909jo32032re2e2e2e32e23ee23',
//amount of milliseconds ! expiry duration
duration : 30*60*1000,
//min active duration.
activeDuration : 5*6*1000,
}));
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function (req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function (req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function (req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function (req, res) {
//if user exits in session
if(req.session && req.session.user){
//if yes! then extract the email from session and compare it to database
User.findOne({email : req.session.user.email}, function(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
}
else{
//if user match with sessions email and database's email then
//set to local variable so we can render the data in jade
res.locals.user = user;
res.render('dashboard.jade');
}
});
}else{
res.redirect('/login');
}
});
//if logout , redirect to Home page
app.get('/logout', function (req, res) {
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function (req, res) {
// res.json(req.body); to get JSON form of data from body parser middleware
//getting the user's information from body parser
var currentUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: req.body.password,
});
currentUser.save(function (err) {
if (err) {
var error = 'Something went Wrong! Try again';
if (err.code === 11000) {
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error: error });
}
else {
res.redirect('/dashboard');
}
});
});
app.post('/login', function (req, res) {
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({ email: req.body.email }, function (err, user) {
//if user doesn't exist then Invalid Error
if (!user) {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
//if user exists check for password
else {
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if (req.body.password == user.password) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('dashboard');
}
//if password doesn't match, redirect to loing page!
else {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
}
});
});
//finally listen on port 3000
app.listen(3000);
Now when you log in, you will be able to see following screen :
This is pretty cool! RIGHT?
we are now able to get the logged in user's detail on our page using session cookie!
Now, lets talk about logout. when user visits localhost:3000/logout we want user to log out and redirect it to home page. To achieve that, add following code in your logout route.
//if logout , redirect to Home page
app.get('/logout', function (req, res) {
//removing user's session cookie so no longer be able to login using that data.
req.session.reset();
res.redirect('/');
});
Now that we have figured out the basic session cookie things, lets see what's happening in the background. This will give you deeper insight on how it works!
When you login and inspect the code in your browser , you will see following details :
copy the headers as i have shown in the above image, and lets check what it contains!
The data might look like this :
HTTP/1.1 302 Found
X-Powered-By: Express
Location: /dashboard
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 64
Set-Cookie: authenticationSession=6jwWO9xVNLC4k_nIeSh5_a3OE3x7gVff9TU-mQ4tYrIQ-TrrcT_iJ4FXXMwV8; path=/; expires=Wed, 13 Jul 2016 06:54:07 GMT; httponly
Date: Wed, 13 Jul 2016 06:24:31 GMT
Connection: keep-alive
This is the data that our server has sent back to us. Take a look at set-cookie: "random_string" . This string contains the user object and will be used for sessions.Remember? We specified 'secret' parameter in while setting up session, that secret string will be used to encrypt and decrypt the data of user object.
Password Encryption using BcryptJs
Open the database and look at the registered users in our database. You will see data as follows :
One thing you might notice is that you are able to see the password of the stored user. It means that if someone else gets hands on our database, they would be able to see the password of all stored user in our database, which is really bad! from a security standpoint.
Password should never be saved as plain text. It should be saved using hashing algorithms. Below are some of very popular hashing algorithms
- md5 (pretty popular in php )
- sha265
- bcrypt
- scrypt
- etc
Hashing password is basically converting your password into random encrypted string which makes no sense to reader, and it can never be turned into its original string. To implement hashing of our password in database, we are going to use brcyptjs library.
Bcryptjs is very popular and one of the best library for hashing password. It's private and best supported. So, lets implement it,
Install the bcryptjs in your project directory
cmd :
your_project_dir > npm install brcyptjs
lets import it in our app.js file.var bcrypt = require('bcryptjs');
Now, if our registration route, instead of storing plain text of our password, we are going to hash the password using bcryptjs and then store it inside the database. You can do that as follows :app.post('/register', function (req, res) {
//generating password hash of what user sent us!
var hashPassword = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10));
//getting the user's information from body parser
var currentUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
//storing the hashed password instead of storing plain text into the database!
password: hashPassword,
});
currentUser.save(function (err) {
if (err) {
var error = 'Something went Wrong! Try again';
if (err.code === 11000) {
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error: error });
}
else {
res.redirect('/dashboard');
}
});
});
Now that we have stored our hashed password in our database, we now have to figure out the way to decrypt it so, user can log in again using the same password.Before this, we were comparing the password stored in database to entered password just like below code :
if (req.body.password == user.password) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('dashboard');
}
above code works when password is stored in plain text, because we are directly comparing it with database's password. But, now that we have our password hashed and secure, we need to use following code and let the bcrypt library decide, if both password are the same. If true, the let the user log in. if (bcrypt.compareSync(req.body.password, user.password)) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('dashboard');
}
And that's all we need to do in order to store the password in our database securely.At this point, your app.js file should look like this!
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var sessions = require('client-sessions');
var bcrypt = require('bcryptjs');
//including express package.
var express = require('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
var User = mongoose.model('User', new Schema({
id: objectId,
firstName: String,
lastName: String,
email: { type: String, unique: true },
password: String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine', 'jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(sessions({
//name of cookie to use in our web application
cookieName : 'session',
//this string will be used by the application to
// encrypt or decrypt the information stored in cookie
secret : 'iw090909jo32032re2e2e2e32e23ee23',
//amount of milliseconds ! expiry duration
duration : 30*60*1000,
//min active duration.
activeDuration : 5*6*1000,
}));
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function (req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function (req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function (req, res) {
res.render('login.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard', function (req, res) {
//if user exits in session
if(req.session && req.session.user){
//if yes! then extract the email from session and compare it to database
User.findOne({email : req.session.user.email}, function(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
}
else{
//if user match with sessions email and database's email then
//set to local variable so we can render the data in jade
res.locals.user = user;
res.render('dashboard.jade');
}
});
}else{
res.redirect('/login');
}
});
//if logout , redirect to Home page
app.get('/logout', function (req, res) {
req.session.reset();
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function (req, res) {
//generating password hash of what user sent us!
var hashPassword = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10));
//getting the user's information from body parser
var currentUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
//storing the hashed password instead of storing plain text into the database!
password: hashPassword,
});
currentUser.save(function (err) {
if (err) {
var error = 'Something went Wrong! Try again';
if (err.code === 11000) {
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error: error });
}
else {
res.redirect('/dashboard');
}
});
});
app.post('/login', function (req, res) {
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({ email: req.body.email }, function (err, user) {
//if user doesn't exist then Invalid Error
if (!user) {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
//if user exists check for password
else {
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if (bcrypt.compareSync(req.body.password, user.password)) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('/dashboard');
}
//if password doesn't match, redirect to loing page!
else {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
}
});
});
//finally listen on port 3000
app.listen(3000);
You can verify your user's password in your database. You will see hashed password as follows :
As you can see in the image, our passwords are now hashed and secure. This is pretty cool right? Of course it is!
Lets go to next step to improve our code a little bit!
USING MIDDLE-WARE
Whenever we want to get any page, we need to write some repetitive code every-time such as getting user's information and compare if its valid, and sessions. So, whenever user jumps from one page to another page, we have to hard code the necessary code for each route. This works, but it gets tedious and generates code that prone to mistakes and errors. To avoid this, we can use middle-ware. We can write middle-ware which would allow us to write less code and get the more work done faster and efficiently.
Lets add following middle-ware, which would run everytime user makes request to visit the page. Add this right after we set up our cookie secret and name information.
//This is going to run everytime user visits any page
app.use(function(req, res, next){
if(req.session && req.session.user){
User.findOne({email: req.session.user.email}, function(err, user) {
if(user){
//setting up the user variable to req.
req.user = user;
//deleting the password from session
delete req.user.password;
//refresh the session value
req.session.user = user;
//making the user available for the dashboard
res.locals.user = user;
}
next();
});
}else{
next();
}
});
Now, this will ensure that if session's user exists, then set up necessary details every time user makes a request. Any page you visit, this will execute first, and then next() will allow it to move forward.
Now, lets add another middle-ware which is function that can check if user is logged in or not, and based on that, redirect user to dashboard or login page. You can add this right after the above middle-ware.
function requiredLogin(req, res, next){
if(!req.user){
res.redirect('/login');
}else{
next();
}
}
This will just check, if the req.user is set up , means user is logged in and if not, then redirect it to login page. If logged in, then next(). so we can use this middle-ware inside dashboardBefore our code was this of dashboard route
app.get('/dashboard',requiredLogin, function (req, res) {
if user exits in session
if(req.session && req.session.user){
//if yes! then extract the email from session and compare it to database
User.findOne({email : req.session.user.email}, function(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
}
else{
//if user match with sessions email and database's email then
//set to local variable so we can render the data in jade
res.locals.user = user;
res.render('dashboard.jade');
}
});
}else{
res.redirect('/login');
}
//res.render('dashboard.jade');
});
But, after adding our middle-ware, we just need to add following code in our dashboard route,
app.get('/dashboard',requiredLogin, function (req, res) {
res.render('dashboard.jade');
});
In above code, when you request for localhost:3000/dashboard
It will first go to global middle-ware for checking if user is logged in using sessions! If true, then it sets up req.user.
After then, it goes to requiredLogin middle-ware, which will see if req.user is set up, then redirect to dashboard , otherwise redirect it to login page.
This way, middle-ware saves us lot of time and make our code cleaner and readable.
In the end, your app.js file should look like this :
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var sessions = require('client-sessions');
var bcrypt = require('bcryptjs');
//including express package.
var express = require('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
var User = mongoose.model('User', new Schema({
id: objectId,
firstName: String,
lastName: String,
email: { type: String, unique: true },
password: String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine', 'jade');
app.locals.pretty = true;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(sessions({
//name of cookie to use in our web application
cookieName : 'session',
//this string will be used by the application to
// encrypt or decrypt the information stored in cookie
secret : 'iw090909jo32032re2e2e2e32e23ee23',
//amount of milliseconds ! expiry duration
duration : 30*60*1000,
//min active duration.
activeDuration : 5*6*1000,
}));
//This is going to run everytime user visits any page
app.use(function(req, res, next){
if(req.session && req.session.user){
User.findOne({email: req.session.user.email}, function(err, user) {
if(user){
//setting up the user variable to req.
req.user = user;
//deleting the password from session
delete req.user.password;
//refresh the session value
req.session.user = user;
//making the user available for the dashboard
res.locals.user = user;
}
next();
});
}else{
next();
}
});
function requiredLogin(req, res, next){
if(!req.user){
res.redirect('/login');
}else{
next();
}
}
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function (req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register', function (req, res) {
res.render('register.jade');
});
//localhost:3000/login
// tells server to open login page
app.get('/login', requiredLogin, function (req, res) {
res.render('dashboard.jade');
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard',requiredLogin, function (req, res) {
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout', function (req, res) {
req.session.reset();
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function (req, res) {
//generating password hash of what user sent us!
var hashPassword = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10));
//getting the user's information from body parser
var currentUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
//storing the hashed password instead of storing plain text into the database!
password: hashPassword,
});
currentUser.save(function (err) {
if (err) {
var error = 'Something went Wrong! Try again';
if (err.code === 11000) {
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error: error });
}
else {
res.redirect('/dashboard');
}
});
});
app.post('/login', function (req, res) {
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({ email: req.body.email }, function (err, user) {
//if user doesn't exist then Invalid Error
if (!user) {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
//if user exists check for password
else {
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if (bcrypt.compareSync(req.body.password, user.password)) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('/dashboard');
}
//if password doesn't match, redirect to loing page!
else {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
}
});
});
//finally listen on port 3000
app.listen(3000);
CSURF
Lets imagine, that we have bank account, and currently we are logged in. Now, banking system has page called bank.com/withdrawMoney. That form contains post request with following fields :
<form method=" post=""><input type="text" name="account"/>
<input type="text" name="amount"/>
<input type="text" name="for"/>
</form>
Now this contains sensitive information. You can send the money to any other person if you pass values in this form. Since you are logged in, and lets say , your bad friend sent you a picture to check out like this :
This will basically execute your form with details passed in url, and bank will perform the transaction on your behalf to send the money even if you haven't intended to. This can cause serious security issue.
In order to secure our platform with this kind of security breach, CSRF token are best solution to this problem. CSRF generates a unique code on every page refresh which is hidden from the form page , and it will specifically unique to your form only. If this doesn't match with unique one then, it fails to submit the information passed through form.
So, lets implement the CSRF token. We have already setup the _csrf name with value csrfToken in our login.jade and register.jade file. Lets install in our app.
cmd :
> npm install csurf
After installing lets initialize it by following :var tokens = require('csurf');
Now, lets use this instance of csurf to create tokens by following. You can add this line before the global middle-ware in our app. app.use(tokens());
This will generate tokens, and now we are ready to use it in our pages.Add on following routes following :
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register',function (req, res) {
res.render('register.jade', {csrfToken : req.csrfToken()});
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function (req, res) {
if(req.session && req.session.user){
res.redirect('dashboard');
}else{
res.render('login.jade',{csrfToken : req.csrfToken()});
}
});
As you can see, we are passing csrfToken (passing to Jade file ) , and this will contain the value of code generated using req.csrfToken(); . This will generate a code on every refresh and make our app secure.
At this point, your app.js should look like this and its final version. This will contain the all functionality we have implemented so far.
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var sessions = require('client-sessions');
var bcrypt = require('bcryptjs');
var tokens = require('csurf');
//including express package.
var express = require('express');
//Import schema
var Schema = mongoose.Schema;
var objectId = Schema.ObjectId;
//Connect to database
mongoose.connect('mongodb://localhost/auth');
var User = mongoose.model('User', new Schema({
id: objectId,
firstName: String,
lastName: String,
email: { type: String, unique: true },
password: String,
}));
//firing up express app.
var app = express();
//Telling the web server to look for jade file for rendering web page.
app.set('view engine', 'jade');
app.use(express.static('./public'));
app.locals.pretty = true;
app.use(bodyParser.urlencoded({ extended: true }));
app.use(sessions({
//name of cookie to use in our web application
cookieName : 'session',
//this string will be used by the application to
// encrypt or decrypt the information stored in cookie
secret : 'iw090909jo32032re2e2e2e32e23ee23',
//amount of milliseconds ! expiry duration
duration : 30*60*1000,
//min active duration.
activeDuration : 5*6*1000,
}));
app.use(tokens());
//This is going to run everytime user visits any page
app.use(function(req, res, next){
if(req.session && req.session.user){
User.findOne({email: req.session.user.email}, function(err, user) {
if(user){
//setting up the user variable to req.
req.user = user;
//deleting the password from session
delete req.user.password;
//refresh the session value
req.session.user = user;
//making the user available for the dashboard
res.locals.user = user;
}
next();
});
}else{
next();
}
});
function requiredLogin(req, res, next){
if(!req.user){
res.redirect('/login');
}else{
next();
}
}
// ----------> ALL GET REQUESTS ----------------//
//basic route localhost:3000/ = index.jade
// this will tell to open index.jade file when localhost is opened.
app.get('/', function (req, res) {
res.render('index.jade');
});
//triggers when typed - localhost:3000/register
// tells server to render register.jade file
app.get('/register',function (req, res) {
res.render('register.jade', {csrfToken : req.csrfToken()});
});
//localhost:3000/login
// tells server to open login page
app.get('/login', function (req, res) {
if(req.session && req.session.user){
res.redirect('dashboard');
}else{
res.render('login.jade',{csrfToken : req.csrfToken()});
}
});
//localhost:3000/dashboard
// tells server to render dashboard.js
app.get('/dashboard',requiredLogin, function (req, res) {
//if user exits in session
// if(req.session && req.session.user){
// //if yes! then extract the email from session and compare it to database
// User.findOne({email : req.session.user.email}, function(err, user){
// if(!user){
// req.session.reset();
// res.redirect('/login');
// }
// else{
// //if user match with sessions email and database's email then
// //set to local variable so we can render the data in jade
// res.locals.user = user;
// res.render('dashboard.jade');
// }
// });
// }else{
// res.redirect('/login');
// }
res.render('dashboard.jade');
});
//if logout , redirect to Home page
app.get('/logout', function (req, res) {
req.session.reset();
res.redirect('/');
});
// ----------> ALL GET REQUESTS ----------------//
app.post('/register', function (req, res) {
//generating password hash of what user sent us!
var hashPassword = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10));
//getting the user's information from body parser
var currentUser = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
//storing the hashed password instead of storing plain text into the database!
password: hashPassword,
});
currentUser.save(function (err) {
if (err) {
var error = 'Something went Wrong! Try again';
if (err.code === 11000) {
error = 'This email is already taken! Try another!';
}
res.render('register.jade', { error: error });
}
else {
res.redirect('/dashboard');
}
});
});
app.post('/login', function (req, res) {
//User object contains all the users from MongoDb
//findOne( match value, function(err, user_Returned))
// we are passing email that we got from body parser
// req.body.email is the name specified in login.jade with name - email.
// it will fetch its value and find one with that email.
User.findOne({ email: req.body.email }, function (err, user) {
//if user doesn't exist then Invalid Error
if (!user) {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
//if user exists check for password
else {
//if email exists, then match with password entered
// got the password entered by user using body parser req.body.password
if (bcrypt.compareSync(req.body.password, user.password)) {
//passing the object to session cookie!
//when user is logged in, that user is passed to session's user
// this will set the set-cookie header in http request
// cookie will be named session and it will have data of user object retrieved!
// currently if user is valid then store in cookie all infor of user
req.session.user = user;
res.redirect('/dashboard');
}
//if password doesn't match, redirect to loing page!
else {
res.render('login.jade', { error: 'Invalid Email Address or Password' });
}
}
});
});
//finally listen on port 3000
app.listen(3000);
Please read my comments to understand the code since i cannot explain each line. You can verify generated tokens by vising localhost/login , and localhost/register page's source code after loading the page.
As you can see in the source code, we have successfully generated csrf code and made our app secure.
At this point, we have fully functioning secure app from scratch. I hope you guys have learned interesting and basic things about authentication. Once again, if you still have any question, problem, or any suggestion, please leave a comment. I would love to hear from you guys!
Thanks!
Happy Coding guys!
2 Comments
ajay
ReplyDeletedsdfgsdfgdf
ReplyDelete