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 on some layout experiments. We notice 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', 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
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" } >
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) {
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;
}
};
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.
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.