Copyright Derek O'Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.
In the previous section of these notes, we saved the user passwords as unencrypted text. In real-world systems, we should never do this. Passwords should always be stored in an encrypted format. Hashing is used to encrypt passwords. The encrypted password is called a hash. It is impossible to turn a hash back into its original unencrypted password.
Use MongoDB Compass to look at the unencrypted passwords that you created in the previous section.
We can use the bcryptjs package to encrypt passwords.
Install the bcryptjs package.
We can use the bcrypt.hash() function to combine an unencrypted password and a saltRounds to produce an encrypted hash.
The bcrypt.hash() function is shown in the code below:
bcrypt.hash(password, saltRounds, (err, hash) => { // hash is the encrypted password. })
The higher the value of saltRounds, the more secure the hash will be, but the longer it will take to generate the hash. In real-world commercial systems, saltRounds should be set to be as high a number as possible. However, it must also be able to generate the hash within one or two seconds. Otherwise, users will be frustrated with having to wait too long everytime that they try to log into the system. In our examples, we set the saltRounds to 3. This will result in the hash being generated very quickly. Obviously, in real-world commerial system, saltRounds should be a higher number.
The saltRounds can be stored in the server/config/.env file, as shown below:
...
PASSWORD_HASH_SALT_ROUNDS = 3 ...
The bcrypt.hash() function is used to hash the password of new users who are registering or for users who change their password. Therefore, it will be used in the file server/routes/users.js, as shown below:
... bcrypt.hash(password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { // hash is the encrypted password }) ...
Open the mongodb_multiple project from the previous section in these notes. Change the code so that a user's password will be hashed when they register. Use MongoDB Compass to check that the passwords are being hashed.
We use the bcrypt.compare() function to compare a password against its hash. This will be called to validate a user's password whenever a user logs into the system. Therefore, it will be used in the file server/routes/users.js, as shown below:
... bcrypt.compare(password, hash, (err, res) =>
{
if(res)
{
// res === true // the password is a valid match for the hash
} else {
// res === false // the password does not match the password
}) }) ...
Open the mongodb_multiple project from the previous section in these notes. Change the code so that a user can log into the system, as shown below.
Use console.log() on the client-side to test that your login code is working.
NOTE: At this point, the login code that you write does not need to restrict a user's access to any component on the client-side. The login code is only being written to show that you are able to use the bcrypt.compare() function and that you are able to get the send the login state from the server to the client.
The full project code for the "Cars" Worked Example that is described below can be downloaded from this link.
This example uses hashing to encrypt the users' passwords that are stored in the users database collection.
import React, {Component} from "react" import {BrowserRouter, Switch, Route} from "react-router-dom" import "bootstrap/dist/css/bootstrap.css" import "./css/App.css" import Login from "./components/Login" import Register from "./components/Register" import ResetDatabase from "./components/ResetDatabase" import AddCar from "./components/AddCar" import EditCar from "./components/EditCar" import DeleteCar from "./components/DeleteCar" import DisplayAllCars from "./components/DisplayAllCars" export default class App extends Component { render() { return ( <BrowserRouter> <Switch> <Route exact path="/Login" component={Login} /> <Route exact path="/Register" component={Register} /> <Route exact path="/ResetDatabase" component={ResetDatabase} /> <Route exact path="/" component={DisplayAllCars} /> <Route exact path="/AddCar" component={AddCar} /> <Route exact path="/EditCar/:id" component={EditCar} /> <Route exact path="/DeleteCar/:id" component={DeleteCar} /> <Route exact path="/DisplayAllCars" component={DisplayAllCars}/> <Route path="*" component={DisplayAllCars}/> </Switch> </BrowserRouter> ) } }
Include the Login, Register and ResetDatabase components in the app.
import React, {Component} from "react"
import {Link} from "react-router-dom"
import axios from "axios"
import CarTable from "./CarTable"
import {SERVER_HOST} from "../config/global_constants"
export default class DisplayAllCars extends Component
{
constructor(props)
{
super(props)
this.state = {
cars:[]
}
}
componentDidMount()
{
axios.get(`${SERVER_HOST}/cars`)
.then(res =>
{
if(res.data)
{
if (res.data.errorMessage)
{
console.log(res.data.errorMessage)
}
else
{
console.log("Records read")
this.setState({cars: res.data})
}
}
else
{
console.log("Record not found")
}
})
}
render()
{
return (
<div className="form-container">
<div>
<Link className="green-button" to={"/Login"}>Login</Link>
<Link className="blue-button" to={"/Register"}>Register</Link>
<Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>
<br/><br/><br/>
</div>
<div className="table-container">
<CarTable cars={this.state.cars} />
<div className="add-new-car">
<Link className="blue-button" to={"/AddCar"}>Add New Car</Link>
</div>
</div>
</div>
)
}
}
Add buttons to link to the Login, Register and ResetDatabase components
<div>
<Link className="green-button" to={"/Login"}>Login</Link>
<Link className="blue-button" to={"/Register"}>Register</Link>
<Link className="red-button" to={"/ResetDatabase"}>Reset Database</Link>
<br/><br/><br/>
</div>
import React, {Component} from "react"
import {Redirect, Link} from "react-router-dom"
import axios from "axios"
import Button from "../components/Button"
import {SERVER_HOST} from "../config/global_constants"
export default class Login extends Component
{
constructor(props)
{
super(props)
this.state = {
email:"",
password:"",
isLoggedIn:false
}
}
handleChange = (e) =>
{
this.setState({[e.target.name]: e.target.value})
}
handleSubmit = (e) =>
{
axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
.then(res =>
{
if(res.data)
{
if (res.data.errorMessage)
{
console.log(res.data.errorMessage)
}
else // user successfully logged in
{
console.log("User logged in")
this.setState({isLoggedIn:true})
}
}
else
{
console.log("Login failed")
}
})
}
render()
{
return (
<form className="form-container" noValidate = {true} id = "loginOrRegistrationForm">
<h2>Login</h2>
{this.state.isLoggedIn ? <Redirect to="/DisplayAllCars"/> : null}
<input
type = "email"
name = "email"
placeholder = "Email"
autoComplete="email"
value={this.state.email}
onChange={this.handleChange}
/><br/>
<input
type = "password"
name = "password"
placeholder = "Password"
autoComplete="password"
value={this.state.password}
onChange={this.handleChange}
/><br/><br/>
<Button value="Login" className="green-button" onClick={this.handleSubmit}/>
<Link className="red-button" to={"/DisplayAllCars"}>Cancel</Link>
</form>
)
}
}
When the user submits the login form, axios is used to send the the email and password to the server-side and to process the response that the server-side returns.
handleSubmit = (e) =>
{
axios.post(`${SERVER_HOST}/users/login/${this.state.email}/${this.state.password}`)
.then(res =>
{
if(res.data)
{
if (res.data.errorMessage)
{
console.log(res.data.errorMessage)
}
else // user successfully logged in
{
console.log("User logged in")
this.setState({isLoggedIn:true})
}
}
else
{
console.log("Login failed")
}
})
}
# This file holds global constants that are visible on the Server-side # Database DB_NAME = D01234567 # Salt length of encryption of user passwords # The salt length should be 16 or higher for commercially released code # It has been set to 3 here, so that the password will be generated faster PASSWORD_HASH_SALT_ROUNDS = 3 # Port SERVER_PORT = 4000 # Local Host LOCAL_HOST = http://localhost:3000
We need to set up server-side global variable for the salt.
# Salt length of encryption of user passwords
# The salt length should be 16 or higher for commercially released code
# It has been set to 3 here, so that the password will be generated faster
PASSWORD_HASH_SALT_ROUNDS = 3
const router = require(`express`).Router() const usersModel = require(`../models/users`) const bcrypt = require('bcryptjs'); // needed for password encryption // IMPORTANT // Obviously, in a production release, you should never have the code below, as it allows a user to delete a database collection // The code below is for development testing purposes only router.post(`/users/reset_user_collection`, (req,res) => { usersModel.deleteMany({}, (error, data) => { if(data) { const adminPassword = `123!"£qweQWE` bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash}, (createError, createData) => { if(createData) { res.json(createData) } else { res.json({errorMessage:`Failed to create Admin user for testing purposes`}) } }) }) } else { res.json({errorMessage:`User is not logged in`}) } }) }) router.post(`/users/register/:name/:email/:password`, (req,res) => { // If a user with this email does not already exist, then create new user usersModel.findOne({email:req.params.email}, (uniqueError, uniqueData) => { if(uniqueData) { res.json({errorMessage:`User already exists`}) } else { bcrypt.hash(req.params.password, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { usersModel.create({name:req.params.name,email:req.params.email,password:hash}, (error, data) => { if(data) { res.json({name: data.name}) } else { res.json({errorMessage:`User was not registered`}) } }) }) } }) }) module.exports = router
We should encrypt the passwords that we are storing in the users collection. We use the bcryptjs package to do this.
const bcrypt = require('bcryptjs'); // needed for password encryption
Use the bcrypt.hash() method to encrypt a password.
bcrypt.hash(stringToEncrypt, HASH_SALT_LENGTH, (err, hash) => { // do something with the hashed password })
When we reset the users collection, we set up an admin user who has the password `123!"£qweQWE`.
router.post(`/users/reset_user_collection`, (req,res) => { usersModel.deleteMany({}, (error, data) => { if(data) { const adminPassword = `123!"£qweQWE` bcrypt.hash(adminPassword, parseInt(process.env.PASSWORD_HASH_SALT_ROUNDS), (err, hash) => { usersModel.create({name:"Administrator",email:"admin@admin.com",password:hash}, (createError, createData) => { if(createData) { res.json(createData) } else { res.json({errorMessage:`Failed to create Admin user for testing purposes`}) } }) }) } else { res.json({errorMessage:`User is not logged in`}) } }) })
Copyright Derek O' Reilly, Dundalk Institute of Technology (DkIT), Dundalk, Co. Louth, Ireland.