Incorporating login and logout routing functions in the Notes application

What we’ve built so far is a user data model, with a REST API wrapping that model to create our authentication information service. Then, within the Notes application, we have a module that requests user data from this server. As yet, nothing in the Notes application knows that this user model exists. The next step is to create a routing module for login/logout URLs and to change the rest of Notes to use user data.

The routing module is where we use passport to handle user authentication. The first task is to install the required modules, as follows:

$ npm install passport@^0.4.x passport-local@1.x –save 

The passport module gives us the authentication algorithms. To support different authentication mechanisms, the passport authors have developed several strategy implementations—the authentication mechanisms, or strategies, corresponding to the various third-party services that support authentication, such as using OAuth to authenticate against services such as Facebook, Twitter, or GitHub.

Passport also requires that we install Express Session support. Use the following command to install the modules:

$ npm install express-session@1.17.x session-file-store@1.4.x –save

The strategy implemented in the passport-local package authenticates solely using data stored locally to the application—for example, our user authentication information service. Later, we’ll add a strategy module to authenticate the use of OAuth with Twitter.

Let’s start by creating the routing module, routes/users.mjs, as follows:

import path from ‘path’; import util from ‘util’;

import { default as express } from ‘express’;

import { default as passport } from ‘passport’;

import { default as passportLocal } from ‘passport-local’;

const LocalStrategy = passportLocal.Strategy;

import * as usersModel from ‘../models/users-superagent.mjs’;

import { sessionCookieName } from ‘../app.mjs’;

export const router = express.Router();

import DBG from ‘debug’;

const debug = DBG(‘notes:router-users’);

const error = DBG(‘notes:error-users’);

This brings in the modules we need for the /users router. This includes the two passport modules and the REST-based user authentication model.

In app.mjs, we will be adding session support so our users can log in and log out. That relies on storing a cookie in the browser, and the cookie name is found in this variable exported from app.mjs. We’ll be using that cookie in a moment.

Add the following functions to the end of routes/users.mjs:

export function initPassport(app) {

app.use(passport.initialize());

app.use(passport.session());

}

export function ensureAuthenticated(req, res, next) {

try {

// req.user is set by Passport in the deserialize function if (req.user) next();

else res.redirect(‘/users/login’);

} catch (e) { next(e); }

}

The initPassport function will be called from app.mjs, and it installs the Passport middleware in the Express configuration. We’ll discuss the implications of this later when we get to app.mjs changes, but Passport uses sessions to detect whether this HTTP request is authenticated. It looks at every request coming into the application, looks for clues about whether this browser is logged in, and attaches data to the request object as req.user.

The ensureAuthenticated function will be used by other routing modules and is to be inserted into any route definition that requires an authenticated logged-in user.

For example, editing or deleting a note requires the user to be logged in and, therefore, the corresponding routes in routes/notes.mjs must use ensureAuthenticated. If the user is not logged in, this function redirects them to /users/login so that they can log in.

Add the following route handlers in routes/users.mjs:

router.get(‘/login’, function(req, res, next) {

try {

res.render(‘login’, { title: “Login to Notes”, user: req.user, });

} catch (e) { next(e); }

});

router.post(‘/login’,

passport.authenticate(‘local’, {

successRedirect: ‘/’, // SUCCESS: Go to home page failureRedirect: ‘login’, // FAIL: Go to /user/login

})

);

Because this router is mounted on /users, all these routes will have /user prepended. The /users/login route simply shows a form requesting a username and password. When this form is submitted, we land in the second route declaration, with a POST on /users/login. If passport deems this a successful login attempt using LocalStrategy, then the browser is redirected to the home page. Otherwise, it is redirected back to the /users/login page.

Add the following route for handling logout:

router.get(‘/logout’, function(req, res, next) {

try {

req.session.destroy();

req.logout();

res.clearCookie(sessionCookieName);

res.redirect(‘/’);

} catch (e) { next(e); }

});

When the user requests to log out of Notes, they are to be sent to /users/logout. We’ll be adding a button to the header template for this purpose. The req.logout function instructs Passport to erase their login credentials, and they are then redirected to the home page.

This function deviates from what’s in the Passport documentation. There, we are told to simply call req.logout, but calling only that function sometimes results in the user not being logged out. It’s necessary to destroy the session object, and to clear the cookie, in order to ensure that the user is logged out. The cookie name is defined in app.mjs, and we imported sessionCookieName for this function.

Add the LocalStrategy to Passport, as follows:

passport.use(new LocalStrategy(

async (username, password, done) => {

try {

var check = await usersModel.userPasswordCheck(username, password);

if (check.check) {

done(null, { id: check.username, username: check.username });

} else {

done(null, false, check.message);

}

} catch (e) { done(e); }

}

));

Here is where we define our implementation of LocalStrategy. In the callback function, we call usersModel.userPasswordCheck, which makes a REST call to the user authentication service. Remember that this performs the password check and then returns an object indicating whether the user is logged in.

A successful login is indicated when check.check is true. In this case, we tell Passport to use an object containing username in the session object. Otherwise, we have two ways to tell Passport that the login attempt was unsuccessful. In one case, we use done(null, false) to indicate an error logging in, and pass along the error message we were given. In the other case, we’ll have captured an exception, and pass along that exception.

You’ll notice that Passport uses a callback-style API. Passport provides a done function, and we are to call that function when we know what’s what. While we use an async function to make a clean asynchronous call to the backend service, Passport doesn’t know how to grok the Promise that would be returned. Therefore, we have to throw a try/catch around the function body to catch any thrown exception.

Add the following functions to manipulate data stored in the session cookie:

passport.serializeUser(function(user, done) {

try {

done(null, user.username);

} catch (e) { done(e); }

});

passport.deserializeUser(async (username, done) => {

try {

var user = await usersModel.find(username);

done(null, user);

} catch(e) { done(e); }

});

The preceding functions take care of encoding and decoding authentication data for the session. All we need to attach to the session is the username, as we did in serializeUser. The deserializeUser object is called while processing an incoming HTTP request and is where we look up the user profile data. Passport will attach this to the request object.

1. Login/logout changes to app.mjs

A number of changes are necessary in app.mjs, some of which we’ve already touched on. We did carefully isolate the Passport module dependencies to routes/users.mjs. The changes required in app.mjs support the code in routes/users.mjs.

Add an import to bring in functions from the User router module, as follows:

import { router as indexRouter } from ‘./routes/index.mjs’;

import { router as notesRouter } from ‘./routes/notes.mjs’;

import { router as usersRouter, initPassport } from ‘./routes/users.mjs’;

The User router supports the /login and /logout URLs, as well as using Passport for authentication. We need to call initPassport for a little bit of initialization.

And now, let’s import modules for session handling, as follows:

import session from ‘express-session’;

import sessionFileStore from ‘session-file-store’;

const FileStore = sessionFileStore(session);

export const sessionCookieName = ‘notescookie.sid’;

Because Passport uses sessions, we need to enable session support in Express, and these modules do so. The session-file-store module saves our session data to disk so that we can kill and restart the application without losing sessions. It’s also possible to save sessions to databases with appropriate modules. A filesystem session store is suitable only when all Notes instances are running on the same server computer. For a distributed deployment situation, you’ll need to use a session store that runs on a network-wide service, such as a database.

We’re defining sessionCookieName here so that it can be used in multiple places. By default, express-session uses a cookie named connect.sid to store the session data. As a small measure of security, it’s useful when there’s a published default to use a different non-default value. Any time we use the default value, it’s possible that an attacker might know a security flaw, depending on that default.

Add the following code to app.mjs:

app.use(session({

store: new FileStore({ path: “sessions” }),

secret: ‘keyboard mouse’,

resave: true, saveUninitialized: true,

name: sessionCookieName

}));

initPassport(app);

Here, we initialize the session support. The field named secret is used to sign the session ID cookie. The session cookie is an encoded string that is encrypted in part using this secret. In the Express Session documentation, they suggest the keyboard cat string for the secret. But, in theory, what if Express has a vulnerability, such that knowing this secret can make it easier to break the session logic on your site? Hence, we chose a different string for the secret, just to be a little different and—perhaps—a little more secure.

Similarly, the default cookie name used by express-session is connect.sid. Here’s where we change the cookie name to a non-default name.

FileStore will store its session data records in a directory named sessions. This directory will be auto-created as needed.

In case you see errors on Windows that are related to the files used by session- file-store, there are several alternate session store packages that can be used. The attraction of the session-file-store is that it has no dependency on a service like a database server. Two other session stores have a similar advantage, LokiStore, and MemoryStore. Both are configured similarly to the session-file-store package. For example, to use MemoryStore, first use npm to install the memorystore package, then use these lines of code in app.mjs:

import sessionMemoryStore from ‘memorystore’;

const MemoryStore = sessionMemoryStore(session);

app.use(session({

store: new MemoryStore({}),

secret: ‘keyboard mouse’,

resave: true,

saveUninitialized: true,

name: sessionCookieName

}));

This is the same initialization, but using MemoryStore instead of FileStore.

Mount the User router, as follows:

app.use(‘/’, indexRouter);

app.use(‘/notes’, notesRouter);

app.use(‘/users’, usersRouter);

These are the three routers that are used in the Notes application.

2. Login/logout changes in routes/index.mjs

This router module handles the home page. It does not require the user to be logged in, but we want to change the display a little if they are logged in. To do so, run the following code:

router.get(‘/’, async (req, res, next) => {

try {

let keylist = await notes.keylist();

let keyPromises = keylist.map(key => { return notes.read(key) });

let notelist = await Promise.all(keyPromises);

res.render(‘index’, {

title: ‘Notes’, notelist: notelist,

user: req.user ? req.user : undefined

});

} catch (e) { next(e); }

});

Remember that we ensured that req.user has the user profile data, which we did in deserializeUser. We simply check for this and make sure to add that data when rendering the views template.

We’ll be making similar changes to most of the other route definitions. After that, we’ll go over the changes to the view templates, in which we use req.user to show the correct buttons on each page.

3. Login/logout changes required in routes/notes.mjs

The changes required here are more significant but still straightforward, as shown in the following code snippet:

import { ensureAuthenticated } from ‘./users.mjs’;

We need to use the ensureAuthenticated function to protect certain routes from being used by users who are not logged in. Notice how ES6 modules let us import just the function(s) we require. Since that function is in the User router module, we need to import it from there.

Modify the /add route handler, as shown in the following code block:

router.get(‘/add’, ensureAuthenticated, (req, res, next) => {

try {

res.render(‘noteedit’, {

title: “Add a Note”,

docreate: true, notekey: “”,

user: req.user, note: undefined

});

} catch (e) { next(e); }

});

We’ll be making similar changes throughout this module, adding calls to ensureAuthenticated and using req.user to check whether the user is logged in. The goal is for several routes to ensure that the route is only available to a logged-in user, and—in those and additional routes—to pass the user object to the template.

The first thing we added is to call usersRouter.ensureAuthenticated in the route definition. If the user is not logged in, they’ll be redirected to /users/login thanks to that function.

Because we’ve ensured that the user is authenticated, we know that req.user will already have their profile information. We can then simply pass it to the view template.

For the other routes, we need to make similar changes.

Modify the /save route handler, as follows:

router.post(‘/save’, ensureAuthenticated, (req, res, next) => {

..

});

The /save route only requires this change to call ensureAuthenticated in order to ensure that the user is logged in.

Modify the /view route handler, as follows:

router.get(‘/view’, (req, res, next) => {

try {

var note = await notes.read(req.query.key); res.render(‘noteview’, {

title: note ? note.title : “”, notekey: req.query.key,

user: req.user ? req.user : undefined,

note: note

});

} catch (e) { next(e); }

});

For this route, we don’t require the user to be logged in. We do need the user’s profile information, if any, sent to the view template.

Modify the /edit and /destroy route handlers, as follows:

router.get(‘/edit’, ensureAuthenticated, (req, res, next) => {

try {

var note = await notes.read(req.query.key); res.render(‘noteedit’, {

title: note ? (“Edit ” + note.title) : “Add a Note”, docreate: false,

notekey: req.query.key,

user: req.user,

note: note

});

} catch (e) { next(e); }

});

router.get(‘/destroy’, ensureAuthenticated, (req, res, next) => {

try {

var note = await notes.read(req.query.key); res.render(‘notedestroy’, {

title: note ? ‘Delete ${note.title}’ : “”, notekey: req.query.key,

user: req.user,

note: note

});

} catch (e) { next(e); }

});

router.post(‘/destroy/confirm’, ensureAuthenticated, (req, res, next)

=> {

..

});

Remember that throughout this module, we have made the following two changes to router functions:

  1. We protected some routes using ensureAuthenticated to ensure that the route is available only to logged-in users.
  2. We passed the user object to the template.

For the routes using ensureAuthenticated, it is guaranteed that req.user will contain the user object. In other cases, such as with the /view router function, req.user may or may not have a value, and in case it does not, we make sure to pass undefined. In all such cases, the templates need to change in order to use the user object to detect whether the user is logged in, and whether to show HTML appropriate for a logged-in user.

4. Viewing template changes supporting login/logout

So far, we’ve created a backend user authentication service, a REST module to access that service, a router module to handle routes related to logging in and out of the website, and changes in app.mjs to use those modules. We’re almost ready, but we’ve got a number of changes left that need to be made to the templates. We’re passing the req.user object to every template because each one must be changed to accommodate whether the user is logged in.

This means that we can test whether the user is logged in simply by testing for the presence of a user variable.

In partials/header.hbs, make the following additions:

<nav class=”navbar navbar-expand-md navbar-dark bg-dark”>

<a class=”navbar-brand” href=’/’><i data-feather=”home”></i></a>

<button class=”navbar-toggler” type=”button”

data-toggle=”collapse” data-target=”#navbarLogIn” aria-controls=”navbarLogIn

aria-expanded=”false”

aria-label=”Toggle navigation”>

<span class=”navbar-toggler-icon”></span>

</button>

{{#if user}}

<div class=”collapse navbar-collapse” id=”navbarLogIn”>

<span class=”navbar-text text-dark col”>{{ title }}</span>

<a class=”btn btn-dark col-auto” href=”/users/logout”>

Log Out <span class=”badge badge-light”>{{ user.username

}}

</span></a>

<a class=”nav-item nav-link btn btn-dark col-auto”

href=’/notes/add’>ADD Note</a>

</div>

{{else}}

<div class=”collapse navbar-collapse” id=”navbarLogIn”>

<a class=”btn btn-primary” href=”/users/login”>Log in</a>

</div>

{{/if}}

</nav>

What we’re doing here is controlling which buttons to display at the top of the screen, depending on whether the user is logged in. The earlier changes ensure that the user variable will be undefined if the user is logged out; otherwise, it will have the user profile object. Therefore, it’s sufficient to check the user variable, as shown in the preceding code block, to render different user interface elements.

A logged-out user doesn’t get the ADD Note button and gets a Log in button. Otherwise, the user gets an ADD Note button and a Log Out button. The Log in button takes the user to /users/login, while the Log Out button takes them to /users/logout. Both of those buttons are handled in routes/users.js and perform the expected function.

The Log Out button has a Bootstrap badge component displaying the username. This adds a little visual splotch in which we’ll put the username that’s logged in. As we’ll see later, it will serve as a visual clue to the user as to their identity.

Because nav is now supporting login/logout buttons, we have changed the navbar- toggler button so that it controls a <div> with id=”navbarLogIn”.

We need to create views/login.hbs, as follows:

<div class=”container-fluid”>

<div class=”row”>

<div class=”col-12 btn-group-vertical” role=”group”>

<form method=’POST’ action=’/users/login’>

<div class=”form-group”>

<label for=”username”>User name:</label>

<input class=”form-control” type=’text’ id=’username’ name=’username’ value=” placeholder=’User Name’/>

</div>

<div class=”form-group”>

<label for=”password”>Password:</label>

<input class=”form-control” type=’password’ id=’password’ name=’password’ value=” placeholder=’Password’/>

</div>

<button type=”submit” class=”btn btn-default”>Submit</button>

</form>

</div>

</div>

</div>

This is a simple form decorated with Bootstrap goodness to ask for the username and password. When submitted, it creates a POST request to /users/login, which invokes the desired handler to verify the login request. The handler for that URL will start the Passport process to decide whether the user is authenticated.

In views/notedestroy.hbs, we want to display a message if the user is not logged in. Normally, the form to cause the note to be deleted is displayed, but if the user is not logged in, we want to explain the situation, as illustrated in the following code block:

<form method=’POST’ action=’/notes/destroy/confirm’>

<div class=”container-fluid”>

{{#if user}}

<input type=’hidden’ name=’notekey’ value='{{#if note}}{{notekey}}{{/if}}’>

<p class=”form-text”>Delete {{note.title}}?</p> 

<div class=”btn-group”>

<button type=”submit” value=’DELETE’ class=”btn btn-outline-dark”>DELETE</button>

<a class=”btn btn-outline-dark” href=”/notes/view?key={{#if note}}{{notekey}}{{/if}}”

role=”button”>Cancel</a>

</div>

{{else}}

{{> not-logged-in }}

{{/if}}

</div>

</form>

That’s straightforward—if the user is logged in, display the form; otherwise, display the message in partials/not-logged-in.hbs. We determine which of these to display based on the user variable.

We could insert something such as the code shown in the following block in partials/not-logged-in.hbs:

<div class=”jumbotron”>

<h1>Not Logged In</h1>

<p>You are required to be logged in for this action, but you are not. You should not see this message. It’s a bug if this message appears.

</p>

<p><a class=”btn btn-primary” href=”/users/login”>Log in</a></p>

</div>

As the text says, this will probably never be shown to users. However, it is useful to put something such as this in place since it may show up during development, depending on the bugs you create.

In views/noteedit.hbs, we require a similar change, as follows:

..

<div class=”container-fluid”>

{{#if user}}

..

{{else}}

{{> not-logged-in }}

{{/if}}

</div>

..

That is, at the bottom we add a segment that, for non-logged-in users, pulls in the not-logged-in partial.

The Bootstrap jumbotron component makes a nice and large text display that stands out nicely and will catch the viewer’s attention. However, the user should never see this because each of those templates is used only when we’ve pre-verified the fact that the user is logged in.

A message such as this is useful as a check against bugs in your code. Suppose that we slipped up and failed to properly ensure that these forms were displayed only to logged-in users. Suppose that we had other bugs that didn’t check the form submission to ensure it’s requested only by a logged-in user. Fixing the template in this way is another layer of prevention against displaying forms to users who are not allowed to use that functionality.

We have now made all the changes to the user interface and are ready to test the login/logout functionality.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *