Authentication with Bcrypt and Sessions




Lesson Objectives

  1. Setup
  2. Explain what bcrypt does
  3. Include bcrypt package
  4. Hash a string using bcrypt
  5. Compare a string to a hashed value to see if they are the same
  6. Explain what a session is
  7. Use express-session package as middleware
  8. Save user information on the session object
  9. Retrieve user information saved on the session object
  10. Update user information saved on the session object
  11. Destroy the session
  12. Enable basic authentication in an express app



Setup

We need a brand new express app to learn with.

This guide contains the steps to create the entire project yourself from scratch.

For class, we'll be using a starter template with the completed project, then walking through a tour of what all the code does and how it works. At the end you'll be able to add additional routes that check req.user to see if a user is logged in.

To use the template:

  • npx degit iscott/node-express-mongo-auth-starter#master expresstagram
  • cd expresstagram
  • npm install
  • edit config/database.js to add your mongodb connection string
  • nodemon to start the server

To create this project from scratch:

  1. Make a directory called expresstagram then change into it
mkdir expresstagram
cd expresstagram/
  1. Create a server.js file
touch server.js
  1. Create a package.json file and accept all defaults using npm init -y
npm init -y
  1. Install base dependencies
npm i express ejs morgan
  1. Set up boilerplate for server.js
// Require modules
const express = require('express');
const morgan = require('morgan');
const port = 3000; 

// Set up express app
const app = express();

// Connect to DB


// Configure the app with app.set()
app.set('view engine', 'ejs');

// Mount middleware with app.use()
app.use(morgan('dev'));
app.use(express.static('public'));
app.use(express.urlencoded({ extended: false }));

// Mount routes with app.use()

// Tell App to listen
app.listen(port, function() {
    console.log(`Express is listening on port:${port}`);
});
  1. Create routes, views, controllers, models, and public directories
mkdir routes views controllers models public
  1. Add the appropriate subdirectories to your public directory
mkdir public/css 
  1. Add base files to your sub directories
touch views/index.ejs routes/index.js controllers/index.js public/css/style.css 
  1. Add some boilerplate html to views/index.ejs
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel='stylesheet' href='/css/style.css' />
    <title>Expresstagram</title>
</head>

<body>
    <header>
        <h1>Expresstagram</h1>
        <ul>
            <li>
                <a href="#">Signup</a>
            </li>
            <li>
                <a href="#">Login</a>
            </li>
        </ul>
    </header>
</body>

</html>
  1. Let's set up our routes and controller actions for our root routes
// inside of routes/index.js

const express = require('express');
const router = express.Router();
const indexCtrl = require('../controllers/index');


router.get('/', indexCtrl.index);

module.exports = router;



// inside of controllers/index.js
module.exports = {
    index
};

function index(req, res) {
    res.render('index');
}
  1. Now we require and mount our index router inside of server.js
// Require modules
const express = require('express');
const morgan = require('morgan');
const port = 3000;
const indexRouter = require('./routes/index');
// ^-- requiring the indexRouter


// more code below...


...don't forget to mount your router


// more code above

// Mount Routes app.use()
app.use('/', indexRouter);



Including Mongoose and Connecting to MongoDB

  1. Create a Cloud Hosted MongoDB
  2. The name of the database will be expresstagram and the first collection name will be users
  3. Install Mongoose
  4. Configure Mongoose in a database config module
  5. Add an event listener that listens to a connection event



Install Mongoose

npm i mongoose



Configure Mongoose in a module



mkdir config
touch config/database.js



const mongoose = require('mongoose');

// 🚨 Don't forget to add your username and password to your connection URI

const connectionURI = 'mongodb+srv://<yourusername>:<yourpassword>@cluster0.oc1n0.mongodb.net/expresstagram?retryWrites=true&w=majority'

// shortcut to mongoose.connection object
const db = mongoose.connection;

mongoose.connect(connectionURI, {
    useNewUrlParser: true,
    useCreateIndex: true,
    useUnifiedTopology: true
});


db.on('connected', function () {
    console.log(`Connected to MongoDB at ${db.host}:${db.port}`);
});



Let's then require our database config module so we can connect to the database as soon as express initializes



const express = require('express');
const morgan = require('morgan');
const port = 3000; 
const indexRouter = require('./routes/index');
// Set up express app
const app = express();

// connect to the database with Mongoose
require('./config/database');

// more code below



Start up the App

Time to check if our app starts up without errors...

nodemon






Explain what bcrypt does

bcrypt is a package that will encrypt passwords using a process known as hashing, so that if a database gets compromised (hacked), people's passwords won't be exposed.




Create a Playground to Explore bcrypt

mkdir playground
touch playground/bcrypt.js



Include the bcrypt package



Here's the standard install

npm i bcrypt



Let's then require the module inside of playground/bcrypt.js

const bcrypt = require('bcrypt');



Salting a Hash using bcrypt

In addition to hashing, bcrypt can perform another process known as "salting". It requires you to generate a salt which is used in the encryption process each time a string is hashed.

If you don't do this, the same string will get hashed to the same value each time. If this were to happen, someone with a common password could hack the database and see who else's hashed password had the same value as theirs and know that they have the same password as them.




const bcrypt = require('bcrypt');

// inside of playground/bcrypt.js

const SALT_ROUNDS = bcrypt.genSaltSync(10);

const password = 'supersecretpassword';

const hashedString = bcrypt.hashSync(password, SALT_ROUNDS);


We can then console log the result like so:

// other code above inside of playground/bcrypt.js

console.log(hashedString)


Let's process this file with node to see the hashed string

node playground/bcrypt



Compare a string to a hashed value to see if they are the same

Because the same string gets encrypted differently every time, we have no way of actually seeing what the value of the string is. We can compare it to another string and see if the two are "mathematically" equivalent.


// other code above inside of playground/bcrypt.js

const isMatch = bcrypt.compareSync('yourGuessHere', hashedString); //returns true or false and assigns value to isMatch

console.log(isMatch);


Bcrypt in a little more depth - Thanks Eric Lewis!




Express With Sessions




Explain what a session is

Cookies are little strings of data that get stored on your computer so that, when you return to a web page, it will remember what you did the last time you were there. You can specify how long a cookie will stay around in a browser before it "expires" or is deleted.

This can be after a specific time has elapsed, or it can end as soon as the user closes their browser.

The problem with cookies is that if you store sensitive information in them (usernames, etc), someone could take the computer and view this sensitive information just by opening up the web browser. Sessions are basically cookies, but the server stores the sensitive info in its own memory and passes an encrypted string to the browser as a cookie. The server then uses this encrypted string to know what was saved on the user's computer.

Sessions typically only last for as long as the user keeps their window open, and aren't assigned a specific date to expire.




Use express-session package as middleware

Here's the standard install

npm i express-session 


Then we can require the module inside our app's main entry module e.g ... server.js

// All other required modules above

const session = require('express-session');


Then we mount session to our middleware stack

// ... other code above

// Mount middleware with app.use()
app.use(morgan('dev'));
app.use(express.static('public'));
app.use(express.urlencoded({ extended: false }));


app.use(session({
    secret: 'supersecret',
    resave: false,
    saveUninitialized: false
}));

// ... more code below



Let's make a little session playground inside of server.js


Right below where we mounted our session middleware, let's add the following placeholders:

app.use(session({
    secret: 'supersecret',
    resave: false,
    saveUninitialized: false
}));

////////// Express Session Playground //////////





////////////////////////////////////////////////


// Mount routes with app.use()
app.use('/', indexRouter);



Save user information on the session object

For each of the routes you create, the req object will now have a session property which is itself an object. At the end of the day, it's still JavaScript, so you can add properties to this object.

////////// Express Session Playground //////////

app.get('/first-route', function(req, res) { // any route will work
    req.session.favFood = 'pizza';
    res.send(req.session);
});

////////////////////////////////////////////////



Retrieve user information saved on the session object

Once you add a property to the session object, you can retrieve it when a user navigates to any other route.

You can also use it to make decisions based on the design of your application.

app.get('/second-route', function(req, res) { 
	if(req.session.favFood === 'pizza') { // test to see if necessary value exists
		//do something if it's a match
	} else {
		//do something else if it's not
	}
});



So, for example, we could do something like this

////////// Express Session Playground //////////

app.get('/first-route', function(req, res) { // any route will work
    req.session.favFood = 'pizza';
    res.send(req.session);
});


// new code below

app.get('/second-route', function(req, res) { 
	if(req.session.favFood === 'pizza') { // test to see if necessary value exists
		res.send('<h1>😎 Pizza Party!! 🍕🎉</h1>');
	} else {
		res.send('<h1>Wait ... you don\'t like pizza? 😢</h1>');
	}
});

////////////////////////////////////////////////



Update information saved on the session object

You can overwrite a session value somewhere else too, just like any other property on a normal JavaScript object.

////////// Express Session Playground //////////

app.get('/first-route', function(req, res) { // any route will work
    req.session.favFood = 'pizza';
    res.send(req.session);
});


app.get('/second-route', function(req, res) { 
	if(req.session.favFood === 'pizza') { // test to see if necessary value exists
		res.send('<h1>😎 Pizza Party!! 🍕🎉</h1>');
	} else {
		res.send('<h1>Wait ... you don\'t like pizza? 😢</h1>');
	}
});

// New code below

app.get('/update-route', function(req, res) { 
    req.session.favFood = 'mom\'s spaghetti';
    res.send(req.session);
});


////////////////////////////////////////////////



Sessions unlock tons of potential in our apps, here's another interesting thing you can do 😎

// How many times visited


app.get('/times-visited', function(req, res) {
    if(req.session.visits) {
        req.session.visits++;
    } else {
        req.session.visits = 1;
    }
    res.send(`<h1>You've visited this page ${req.session.visits} time(s)</h1>`);
});



Destroy the session

Lastly, you can forcibly destroy a session like so:

app.get('/destroy-route', function (req, res) { //any route will work
	req.session.destroy(function(err) {
		if(err){
			//do something if destroying the session fails
		} else {
			//do something if destroying the session succeeds
		}
	});
});



NOTE: If you restart your server, it will lose all memory of the sessions it created




Authentication Build

It's time to add authentication to our application! 🎉



First let's add a URI to the signup "navigation link" inside of views/index.ejs

<ul>
    <li>
        <a href="/users/new">Signup</a>
    </li>
    ...



Signup Route

Let's build a router to handle our app's authentication needs

touch routes/users.js



Here's some starter code:

// inside of routes/users.js

const express = require('express');
const router = express.Router();
const usersCtrl = require('../controllers/users');


router.get('/new', usersCtrl.new);

module.exports = router;



Then, we need to mount the router to server.js on /users

// other code above

const usersRouter = require('./routes/users');


// more code here


app.use('/users', usersRouter);

// more code below



Create an Users controller


It's time to create our users controller

touch controllers/users.js


Here's some starter code for our controller

// inside of controllers/users.js

module.exports = {
    new: newUser
};

function newUser(req, res) {
    res.render('users/new');
}



Create a signup view

First, let's make the template inside a dedicated folder inside of views

mkdir views/users

touch views/users/new.ejs



Here's some boilerplate markup for our view

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel='stylesheet' href='/css/style.css' />
    <title>Expresstagram</title>
</head>

<body>
    <h1>Signup</h1>
    <form action="/users/signup" method="POST">
        Username: <input type="text" name="username" /><br />
        Password: <input type="password" name="password" /><br />
        <input type="submit" value="Signup" />
    </form>
</body>

</html>



At this point, we should be able to navigate to our signup page at http://localhost:3000/users/new




Create a user model

Before we try to "signup a user" we should create and export a user model

touch models/user.js



Here's what our model should look like:

// inside of models/user.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
    username: String,
    password: String
}, {
    timestamps: true
});

module.exports = mongoose.model('User', userSchema);
 



Create a signUp route

let's set up a signup route to map over requests made by the signup form

// inside of routes/users.js

const express = require('express');
const router = express.Router();
const usersCtrl = require('../controllers/users');


router.get('/new', usersCtrl.new);
router.post('/signup', usersCtrl.signUp); // new route definition

module.exports = router;



Create a signup controller action

Now we must define and export a controller action that will be used to create a new user in the database

// inside of controllers/users.js
const User = require('../models/user'); // require user model
const bcrypt = require('bcrypt');       // require bcrypt module
const SALT_ROUNDS = 10;                 // the salt round we'll use 


module.exports = {
    new: newUser,
    signUp
};

function newUser(req, res) {
    res.render('auth/new');
}

function signUp(req, res) {
    // we'll add more code here soon
}


Once the form is submitted, we can set req.password to an encrypted version

// inside of controllers/users.js

function signUp(req, res) {
    req.body.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(SALT_ROUNDS));
    res.send(req.body);
}



Create user and then redirect them back home /

// inside of controllers/users.js

function signUp(req, res) {
    req.body.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(10));
    User.create(req.body, function (error, newUser) {
        console.log(newUser) // let's check out our new user
        res.redirect('/');
    });
}



Login Route



let's add a URI to the login "navigation link" inside of views/index.ejs

 <ul>
     <li>
         <a href="/users/new">Signup</a>
     </li>
     <li>
         <a href="/users/signin">Login</a>
     </li>
     ...



Then, inside of routes/users.js

const express = require('express');
const router = express.Router();
const usersCtrl = require('../controllers/users');


router.get('/new', usersCtrl.new);
router.post('/signup', usersCtrl.signUp);

router.get('/signin', usersCtrl.signIn); // new route definition

module.exports = router;



Let's add a controller action that can render a login form

// inside of controllers/users.js
const User = require('../models/user'); // require user model
const bcrypt = require('bcrypt');       // require bcrypt module
const SALT_ROUNDS = 10;                 // the salt round we'll use 


module.exports = {
    new: newUser,
    signUp,
    signIn // new controller action exported
};

function newUser(req, res) {
    res.render('users/new');
}

function signUp(req, res) {
    req.body.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(SALT_ROUNDS));
    User.create(req.body, function (error, newUser) {
        console.log(newUser) // let's check out our new user
        res.redirect('/');
    });
}

// new controller action defined
function signIn(req, res) {
    res.render('users/login');
}



At this point we'll need a view for a user to login in with, so we'll name it login.ejs

touch views/users/login.ejs



Login Page



Let's add this markup to views/users/login.ejs:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel='stylesheet' href='/css/style.css' />
    <title>Expresstagram</title>
</head>

<body>
    <h1>Login</h1>
    <form action="/users/login" method="POST">
        Username: <input type="text" name="username" /><br />
        Password: <input type="password" name="password" /><br />
        <input type="submit" value="Login" />
    </form>
</body>

</html>



Create a login route

Let's add our next route definition

// inside of routes/users.js

const express = require('express');
const router = express.Router();
const usersCtrl = require('../controllers/users');


router.get('/new', usersCtrl.new);

router.post('/signup', usersCtrl.signUp);

router.get('/signin', usersCtrl.signIn);

router.post('/login', usersCtrl.login); // new route definition 

module.exports = router;




Now we just need to export our login action

// inside of controllers/users.js
const User = require('../models/user'); // require user model
const bcrypt = require('bcrypt');       // require bcrypt module
const SALT_ROUNDS = 10;                 // the salt round we'll use 


module.exports = {
    new: newUser,
    signUp,
    signIn,
    login
};

function newUser(req, res) {
    res.render('users/new');
}

function signUp(req, res) {
    req.body.password = bcrypt.hashSync(req.body.password, bcrypt.genSaltSync(SALT_ROUNDS));
    User.create(req.body, function (error, newUser) {
        console.log(newUser) // let's check out our new user
        res.redirect('/');
    });
}

// new controller action defined
function signIn(req, res) {
    res.render('users/login');
}

// here's the login action

function login(req, res) {
    User.findOne({
        username: req.body.username
    }, function (error, foundUser) {
        res.send(foundUser);
    });
}



It's time to refactor the login controller action to redirect to the home screen or same page based on whether password is correct:

function login(req, res) {
    User.findOne({
        username: req.body.username
    }, function (error, foundUser) {
        if (foundUser === null) {
            res.redirect('/users/signin');
        } else {
            const doesPasswordMatch = bcrypt.compareSync(req.body.password, foundUser.password);
            if (doesPasswordMatch) {
                res.redirect('/');
            } else {
                res.redirect('/users/signin');
            }
        }
    });
}



Now we can make a small change to add user information to the session

function login(req, res) {
    User.findOne({
        username: req.body.username
    }, function (error, foundUser) {
        if (foundUser === null) {
            res.redirect('/users/signin');
        } else {
            const doesPasswordMatch = bcrypt.compareSync(req.body.password, foundUser.password);
            if (doesPasswordMatch) {
                req.session.userId = foundUser._id; // new code right here
                console.log(req.session) // we can also log out the session to see the results
                res.redirect('/');
            } else {
                res.redirect('/users/signin');
            }
        }
    });
}



Summary

We've just discussed how to use bcrypt to perform encryption on plain-text passwords and how to use the express-session middleware to create a sessions for our application. We also provided an example of how you can add user information to the session object; this will be highly useful when you want to gate certain content based on whether the requesting user has "logged in" and created a session with our application.

Please see the references below for more information regarding these technologies.





Activity

Make the navigation respond to whether a user is logged in or not.

When a user is not logged in, it should show only:

  • Signup
  • Login

When a user is logged in, it should only show:

  • Logout

HINT: use req.user in the controller.

HINT #2: Pass req.user into the view and write some logic in your EJS tags.





Activity 2

Add the ability to track inventory in this app!

Set up RESTful routes for

/products

Use the REST chart for guidance to set up index, new, and create actions.

HINT: routes should be:

index:  GET /products
new:    GET /products/new
create: POST /products

Your Product schema should accept:

name: String
quantity: Integer

BONUS: Require users to be logged in before they can access the /products/new page





References