Hashing passwords is a security measure mean to protect our applications against unauthorized use. In this section we shall go deeper into authentication, what it is, and work with in practice in the context of Node.js and Express.
Choosing a password has been dealt with somewhere else. You may have seen the arguably most referenced comic in computer science https://xkcd.com/936/. When interpreting that it begs the question: "What is entropy?" A good place to start is https://explainxkcd.com/wiki/index.php/936:_Password_Strength, and think hard. And do remember that a password becomes exponentially more difficult to guees with its length. Take a pincode, 10 possible digits, length 4 gives
104 = 10.000
codes to choose from. Make it longer eg to 8 characters, you get
108 = 100.000.000
possible values. Assume that instead of making it longer, you make it alphanumeric, 26 lower case letters and 10 digits. Then you get
364 = 1.679.616
Let us use a suspicious word: mathematics. Increasing length
of a password you are playing with an exponential function
f(x) = nx.
Play with vocabulary, possible characters, you give yourself
a power function
f(x) = xn.
Exponential growth happens much faster than power
growth when you vary x.
Check out https://en.wikipedia.org/wiki/Exponentiation#Power_functions if you're interested in the math.
A user comes to our application, saying I want to use this. We say sure thing, just present your credentials, and we will check you have authorization , and then we will ask for a userid, and a password. We check these two items, and if both meet our scrutiny, the user is let in. This process is called authentication.
The userid is the publicly known identity of our user, his name or initials as known from his email address, possibly even his total email address. The password is a secret code, word or phrase, that the user supplies so our application may ascertain the the given userid really belongs this user.
A capricious angle on this. The userids in a system must of course be unique, they uniquely identify the individual users of the system. The secrets, the passwords, are different. There is a, infinitely small perhaps, positive probability that all users have the same password. We do not know, and we can never know. They are secret. That is why they are never, as in never, stored as plaintext.
To remain secret passwords are stored as digests, hashes, and they should be hashed with the best possible hashing algorithm. There is no excuse for less. Now we turn to verification of the userid and password.
Let the setting be a playground based om some layout experiments. We noteice the menus.
We see menu items for registration and login. This authentication process is carried out when you log in, or log on to a system. Clicking login gives us:
When the process is succesfully completed, you are authenticated and might see the following:
The greeting at the top of the screen is the only visible cue to the fact that we are now logged in to the system.
Authentication is about people, users, wanting access to the system. So first, the user:
nodeAuthDemo/models/User.js
const mongoose = require("mongoose");
const userSchema = mongoose.Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true,
unique: true
},
created: {
type: Date,
default: Date.now
}
});
userSchema.methods.getFullName = function () {
return `Name: ${this.firstName} ${this.lastName}`;
}
userSchema.methods.getInfo = function () {
return `${this.getFullName()}, Email: ${this.email}, Zipcode: ${this.zipcode}`;
}
userSchema.methods.getCredentials = function () {
return `${this.email}\t${this.password}`;
}
module.exports = mongoose.model("User", userSchema, 'user');
The properties of the user are arbitrary, whatever we need for a give application, or perhaps for a whole series of applications of an organization.
There must be one property identifying the user uniquely, and there must be one property holding the hashed password. I repeat, never, ever store passwords as plaintext.
Obviously the users must come from somewhere. Here follows the registration screen for the users. Its content reflects the object we just saw. Double entry of the password is meant to improve the odds that the user will remember it.
The route to the registration handling is
nodeAuthDemo/routes/users.js
router.get('/register', function(req, res) { // display register route
res.render('register', { // display register form view
title: 'nodeAuthDemo Register User' // input data to view
});
});
router.post('/register', function(req, res) { // new user post route
userHandler.upsertUser(req);
return res.redirect('/'); // skip the receipt, return to fp
});And the code for the database activity storing the user
nodeAuthDemo/models/handleUsers.js
"use strict";
const mon = require("./mongooseWrap");
const bcrypt = require('bcryptjs'); // added for hashing
const User = require("./User");
const saltTurns = 10;
const dbServer = "localhost";
const dbName = "testuser1";
exports.upsertUser = async function (req) {
let check = { email: req.body.email };
let user = new User({
firstName: req.body.firstName,
lastName: req.body.lastName,
email: req.body.email,
password: await bcrypt.hash(req.body.password, saltTurns)
});
try {
let cs = await mon.upsert(dbServer, dbname, User, user, check);
} catch(e) {
console.error(e);
}
};
Resulting in
nmlX240 webexit $ mongo
Enter password:
> use test111
switched to db testUser1
> db.user.find().pretty()
{
"_id" : ObjectId("5e7247de0eb5f28fff216c1f"),
"email" : "nmla@iba.dk",
"__v" : 0,
"created" : ISODate("2020-03-18T16:10:06.011Z"),
"firstName" : "Niels",
"lastName" : "Larsen",
"password" : "$2a$10$JH2KAy25i2xYu83GcIt9YOWMNZ1P0exftII.nvh4A4qJIgf91BvPC"
}
>
Was explaining cookies to my mentee. One thought that was haunting me the whole time: holy shit do we really base our auth systems on that? (tweet by @valueof (Anton Kovalyov, SF, CA), 2013-04-04.)
A systems state is the values of all its parameters. In a computer system these values are stored in data structures, aka variables. The problem with the World Wide Web is that it consists of some server activity, then it rests while a user looks at its result on his screen. The user interacts, a request sets off new server activity resulting in the next manifestation on the screen of the user. The problem is, that these intermittent server activities are separate. One spell doesn't know the previous one, and has no clue what the user interaction will bring next. Every server activity starts as it was the first and only. This is the web's statelessness.
Imagine that you login to gain access to a page of an application. You do what you do, and your are presented with some menu options of what to do next. You choose one only to be faced with the requirement of logging in again, because the application does not know who you are and whether you are authorized to gain access to what you want to do. This is the crippling result of this statelessness. No user would accept this situation.
The last paragraph suggests a possible solution, the only one, to the problem. We must provide some sort of memory that will be accessible from the separate spells of server activity. This memory must be accessible from the server as well as the client. Think in terms of a server with many clients. The memory of a process must be able to link the server activity with a particular client, and vice versa, otherwise there will be chaos.
The answer is sessions, the 42 of the World Wide Web.
nodeAuthDemo/app.js
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const bodyParser = require("body-parser"); // added for POST data handling
const session = require('express-session'); // added for state
const indexRouter = require('./routes/index'); // router for basic routing file
const usersRouter = require('./routes/users'); // router concerned with users routing file
const app = express();
app.locals.pretty = app.get('env') === 'development'; // pretty print html
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(session({secret: 'aaahhhhh', resave: true, saveUninitialized: true})); // setup session
app.use(bodyParser.urlencoded({ extended: false })); // added POST data handling
app.use(bodyParser.json()); // added POST data handling
app.use('/', indexRouter); // urls pointing router index.js
app.use('/users', usersRouter); // urls for users.js
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;nodeAuthDemo/models/handleUsers.js
exports.verifyUser = async function (req) {
let check = { email: req.body.email };
let u = await this.getUsers(check);
let success = await bcrypt.compare(req.body.password, u[0].password);
if (success) {
req.session.authenticated = true; // set session vars
req.session.user = u[0].firstName; // set session vars
} else {
req.session = undefined;
}
return success;
};nodeAuthDemo/routes/users.js
router.get('/login', function(req, res) { // display register route
res.render('login', { // display register form view
title: 'nodeAuthDemo User Login' // input data to view
});
});
router.post('/login', async function(req, res) {// new user post route
let rc = await userHandler.verifyUser(req); // verify credentials
if (rc) {
res.render('index', { // find the view 'index'
title: 'nodeAuthDemo Home', // input data to 'index'
loggedin: true,
who: req.session.user // using session var(s)
});
} else {
res.render('login', { // find the view 'login'
title: 'nodeAuthDemo User Login', // input data to 'login'
loggedin: false
});
}
});
The req.session object is now available in any
function in the application. Let us take a look at the
request object's header content following a successful
login, fragment of the log:
...
sessionID: 'KemecS_xPT-j13R3J5lGyPwvjmteHAE2',
session: Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
authenticated: true,
user: 'Niels'
},
...
The sessionID is used for the server and client
to know who is who. Remember the server has, potentially,
many clients.
The pseudo code for authentication could be summarised to:
app.js.
The router/controller should then be required to check for authentication before displaying any restricted views. Bear in mind that even in an application with restrictions, there may be unrestricted views.