46.3. Authentication

46.3.1. A First Glimpse

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.

Naively
  1. User enters id and password
  2. System reads user info in database, ie id and password
  3. System compares the two passwords
They must compare false because the entered password is plaintext, and the password from the database is hashed. There is no way they will ever be equal.
Realistically
  1. User enters id and password
  2. System reads user info in database, ie id and password
  3. User entered password is hashed
  4. System compares the two passwords
This may work. The comparison may be true. If the user gave the correct password, and the applied hashing algorithm is not bcrypt.
Best Practice
  1. User enters id and password
  2. System reads user info in database, ie id and password
  3. System uses salt from the database password to hash the user entered password. This assures that even though bcrypt hashes a given passwords differently each time, it hashed correct if we use the salt from the stored password. This works only because the salt is stored as part of the password.
  4. System compares the two passwords
This works. The comparison will be true if the user gave the correct password.

46.3.2. The Manifestation

Let the setting be a playground based on some layout experiments. We notice the menus.

Figure 46.1. The Site
The Site

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:

Figure 46.2. Login Screen
Login Screen

When the process is succesfully completed, you are authenticated and might see the following:

Figure 46.3. Logged In
Logged In

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.

46.3.3. The Ingredients

Authentication is about people, users, wanting access to the system. So first, the user:

Example 46.1. The User Schema and Object in Mongoose, 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.

Figure 46.4. User Registration Form
User Registration Form

The route to the registration handling is

Example 46.2. Fragment of 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', async function(req, res) {   // new user post route
    let user = await userHandler.saveUser(req);
    res.redirect('/');                   // skip the receipt, return to fp
});

And the code for the database activity storing the user

Example 46.3. The User Handler, Fragment of nodeAuthDemo/models/handleUsers.js
"use strict";
const bcrypt = require('bcryptjs');                         // added for hashing
const mongoose = require('mongoose');                       // added for mongo
const User = require("./User");

const monConnect = async function () {
    const dbServer = "localhost";
    const dbName = "testuser1";
    const constr = `mongodb://${dbServer}:27017/${dbName}`;
    const conparam = {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useFindAndModify: false,
        useCreateIndex: true
    };
    await mongoose.connect(constr, conparam);
    return mongoose.connection;
};

exports.saveUser = async function (req) {
    const db = await monConnect();
    const saltTurns = 10;
    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 {
        await user.save(function(err, saved) {
            db.close();
            return saved;
        });
    } 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"
}
>

46.3.4. Usage, Authentication

46.3.5. The Problem of, and the Solution to the Web's Statelessness

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.

Example 46.4. Sessions into the Application, 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;

Example 46.5. Setting the Common Memory, Fragment of nodeAuthDemo/models/handleUsers.js
exports.verifyUser = async function (req) {
    const db = await monConnect();
    let check = { email: req.body.email };
    let u = await this.getUsers(check);
    if (u.length === 1) {
        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.destroy();                  // same as logout
        }
        return success;
    } else {
        req.session.destroy();
        return false;
    }
};

Example 46.6. Using the Common Memory, Fragment of 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
        loginerr: false
    });
});
router.post('/login', async function(req, res) {// new user post route
    let rc = await userHandler.verifyUser(req); // verify credentials
    if (rc) {
        res.redirect('/');
    } else {
        res.render('login', {                   // find the view 'login'
            title: 'nodeAuthDemo User Login',   // input data to 'login'
            loggedin: false,
            loginerr: true
        });
    }
});
router.get('/logout', async function(req, res) {      // logout
    await req.session.destroy();
    res.redirect('/');
});

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.

46.3.6. The Programming

The pseudo code for authentication could be summarised to:

  1. Introduce session middleware into the app.js.
  2. Receive id and password (from login form)
  3. Read user, and password matching user id
  4. Hash entered password
  5. Compare read password with hashed entered password
  6. If successful read, set session var to signify succesfull authentication
  7. If unsuccessful issue error, make sure session variable to signify success is not set
  8. Go to menu

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.