left-icon

Keystone.js Succinctly®
by Manikanta Panati

Previous
Chapter

of
A
A
A

CHAPTER 7

Authenticating Users

Authenticating Users


Most dynamic web applications allow for some kind of user authentication and preference-saving functionality. Let us look at how to allow users to create an account and log in and out from our Keystone.js application.

Sessions & configuration

Sessions are used to track user activity on the server side and on the client side via cookies. They are generally used to save pieces of authentication data and user preferences. Sessions can either be stored in memory or can be persisted to storage such as MongoDB or Redis. Keystone.js supports storing sessions in MongoDB via the connect-mongo library. We can also save sessions to in-memory, Redis, Memcached, or a custom session store that we can implement.

There are a few configuration options that need to be set before using the session functionality. These options should be set in the keystone.init() function within the keystone.js file. The configuration options are:

  • session: Set this option to true if you want your application to support session management.
  • auth: This option indicates whether to enable built-in authentication for keystone’s Admin UI, or a custom function to use to authenticate users.
  • user model: This option indicates to Keystone.js which model will be used to maintain the user information.
  • cookie secret: Use this option to specify the encryption key to use for your cookies.
  • session store: This identifies which session storage option to use (in-memory, mongo, etc.).

To use MongoDB as the session store, we need to install connect-mongo as shown.

Code Listing 70: Install connect-mongo

npm install connect-mongo –save

After enabling the session-based authentication options, let us define routes that will be used for authentication. Add the following routes to the route index file.

Code Listing 71: Authentication routes

app.all('/join', routes.views.join);

app.all('/signin', routes.views.signin);

app.get('/signout', routes.views.signout);

Create an account

The first step in allowing users to create an account is to display the registration form. Create a file named join.swig in the templates/views folder with the following content.

Code Listing 72: Registration form markup

{% extends "../../layouts/default.swig" %}

{% block content %}

<div class="container">

    <div class="panel panel-primary">

    <div class="panel-heading">Create An Account</div>

    <div class="panel-body">

     <div class="col-md-6">

    <form action="/join" method="post" class="form-horizontal">

      <fieldset>

      <div class="form-group required">

          <label class="col-md-4 control-label">User Name*</label>

          <div class="col-md-8">

            <input class="form-control" id="username" placeholder="Pick a user name" name="username" type="text" value="{{form.username}}">

          </div>

        </div>

          <div class="form-group required">

          <label class="col-md-4 control-label">First Name*</label>

          <div class="col-md-8">

            <input class="form-control" id="firstname" placeholder="First name" name="firstname" type="text" value="{{form.firstname}}">

          </div>

        </div>

          <div class="form-group required">

          <label class="col-md-4 control-label">Last Name*</label>

          <div class="col-md-8">

            <input class="form-control" id="lastname" placeholder="Last name" name="lastname" type="text" value="{{form.lastname}}">

          </div>

        </div>

          <div class="form-group required">

          <label class="col-md-4 control-label">Email Address*</label>

          <div class="col-md-8">

            <input class="form-control" id="email" placeholder="Email address" name="email" type="email" value="{{form.email}}">

          </div>

        </div>

          <div class="form-group required">

          <label class="col-md-4 control-label">Password*</label>

          <div class="col-md-8">

            <input class="form-control" id="password" name="password" placeholder="password" type="password">

          </div>

        </div>

          <div class="form-group">

          <label class="col-md-4 control-label"></label>

          <div class="col-md-8">

           

            <div style="clear:both"></div>

                <button class="btn btn-primary" type="submit">Join</button>

              </div>

        </div>

      </fieldset>

    </form>

    </div>

 </div>

  </div>

   </div>               

 {% endblock %}

The rendered markup will look like the following figure.

Create account form

Figure 20: Create account form

Create the view named join.js under the /routes/views directory with the following code.

Code Listing 73: Registration view

var keystone = require('keystone'),

     async = require('async');

exports = module.exports = function (req, res) {

    if (req.user) {

        return res.redirect('/');

    }

    var view = new keystone.View(req, res),

          locals = res.locals;

    locals.section = 'createaccount';

    locals.form = req.body;

    view.on('post', function (next) {

        async.series([

               function (cb) {

                   if (!req.body.username || !req.body.firstname || !req.body.lastname || !req.body.email || !req.body.password) {

                       req.flash('error', 'Please enter a username, your name, email and password.');

                       return cb(true);

                   }

                   return cb();

               },

            function (cb) {

                keystone.list('User').model.findOne({ username: req.body.username }, function (err, user) {

                    if (err || user) {

                        req.flash('error', 'User already exists with that Username.');

                        return cb(true);

                    }

                    return cb();

                });

            },

               function (cb) {

                   keystone.list('User').model.findOne({ email: req.body.email }, function (err, user) {

                       if (err || user) {

                           req.flash('error', 'User already exists with that email address.');

                           return cb(true);

                       }

                       return cb();

                   });

               },

               function (cb) {

                   var userData = {

                       username: req.body.username,

                       name: {

                           first: req.body.firstname,

                           last: req.body.lastname,

                       },

                       email: req.body.email,

                       password: req.body.password

                   };

                   var User = keystone.list('User').model,

                         newUser = new User(userData);

                   newUser.save(function (err) {

                       return cb(err);

                   });

               }

        ], function (err) {

            if (err) return next();

            var onSuccess = function () {

                res.redirect('/');

            }

            var onFail = function (e) {

                req.flash('error', 'There was a problem signing you up, please try again.');

                return next();

            }

            keystone.session.signin({ email: req.body.email, password: req.body.password }, req, res, onSuccess, onFail);

        });

    });

    view.render(join');

}

The view uses the excellent async library that performs multiple operations in series. The first (anonymous) function checks if the form inputs have been populated. The next method checks if the username entered on the form already exists. If it exists, we return an error to the user. The series operations terminate at this point. The next method checks if there is an existing user with the same email address.

After all these operations have successfully completed, the user object is constructed and saved to the database. On success, code to log in the user is called and the user is redirected to the homepage.

To test the flash messages that render the error messages from failed form validation, submit the form without filling any values. The error should appear as shown in the following figure.

Form validation errors

Figure 21: Form validation errors

Since we used the app.all method to define the route, both GET and POST are directed to a single action URL. During a GET, the form is rendered, and during a POST, the form is validated.

Sign in and sign out

Sign in

Now that the users are able to create an account, let us look at displaying a login form where the users can authenticate themselves. Add the following code within a new file named signin.swig and save it under the templates/views folder.

Code Listing 74: Sign-in markup

{% extends "../layouts/default.swig" %}

{% block content %}

<div class="container">

    <div class="panel panel-primary">

    <!-- Default panel contents -->

    <div class="panel-heading">Login to Nodepress</div>

    <div class="panel-body">

    <div class="col-md-4">

    <form role="form" action="/signin" method="post">

    <div class="form-group">

        <label for="sender-email" class="control-label">Email address:</label>

        <div class="input-icon"> 

        <input class="form-control email" id="signin-email" placeholder="[email protected]" name="email" type="email" value="">

        </div>

    </div>

    <div class="form-group">

        <label for="user-pass" class="control-label">Password:</label>

        <div class="input-icon">

        <input type="password" class="form-control" placeholder="Password" name="password" id="password">

        </div>

    </div>

    <div class="form-group">

        <input type="submit" class="btn btn-primary " value="Login">   

    </div>

    </form>

    </div>

    </div>

    </div>

</div>

{% endblock %}

The markup for our login form is pretty straightforward. We have defined input fields for the user's email address and password. The form will POST to the /signin URL. If there are errors during user authentication such as invalid email or password, we display those errors using the FlashMessages.renderMessages static method that is offered by Keystone.js. We have included the following piece of code in our layout file /templates/layouts/Default.swig to render the flash messages.

Code Listing 75: Flash messages

{{ FlashMessages.renderMessages(messages) }}

Create the view named signin.js under the /routes/views directory with the following code.

Code Listing 76: Sign-in view

var keystone = require('keystone'),

     async = require('async');

exports = module.exports = function (req, res) {

    if (req.user) {

        return res.redirect('/mytickets');

    }

    var view = new keystone.View(req, res),

          locals = res.locals;

    locals.section = 'signin';

    view.on('post', function (next) {

        if (!req.body.email || !req.body.password) {

            req.flash('error', 'Please enter your email and password.');

            return next();

        }

        var onSuccess = function () {

            res.redirect('/);

        }

        var onFail = function () {

            req.flash('error', 'Input credentials were incorrect, please try again.');

            return next();

        }

        keystone.session.signin({ email: req.body.email, password: req.body.password }, req, res, onSuccess, onFail);

    });

    view.render('signin');

}

In the view, we check whether the user has already logged in. If they have logged in, we redirect the user to the homepage. If the user has not logged in and has submitted the login form, we will process the login request. To validate the form contents, we check if the user has provided an email address and a password. If either one is empty, we set a flash error indicating the missing data and return the callback. If the user has provided valid credentials, then the function will regenerate a new session and complete the sign-in process.

The rendered login form will look like the following figure.

Login form

Figure 22: Login form

Sign out

To sign a user out, we should call the keystone.session.signout method. The signout operation will clear the user's cookies, set the request user object to null, and regenerate a new session. Upon completion, the user will be redirected to the homepage.

Create a view named signout.js under the /routes/views directory with the following code.

Code Listing 77: Sign-out view

var keystone = require('keystone');

exports = module.exports = function (req, res) {

    keystone.session.signout(req, res, function () {

        res.redirect('/');

    });

};

Authentication middleware

Keystone.js has built-in middleware that can be leveraged as part of a request and response cycle. This is especially useful to check if requests need to be blocked or allowed based on whether the user has authenticated themselves.

To restrict access to a route to be accessible only to authenticated users, we can rely on Keystone.js middleware. The middleware exposes a requireUser method that prevents people from accessing protected pages when they’re not signed in. We can apply the middleware to the route as follows.

Code Listing 78: Protect route via middleware

app.all('/profile*', middleware.requireUser);

The preceding piece of code applies the requireUser method before a request reaches any route that follows /profile.

The middleware code resides within the routes/middleware.js file. The requireUser method is implemented as shown in the following snippet.

Code Listing 79: RequireUser middleware

/**

     Prevents people from accessing protected pages when they're not signed in.

 */

exports.requireUser = function (req, res, next) {

    if (!req.user) {

        req.flash('error', 'Please sign in to access this page.');

        res.redirect('/keystone/signin');

    } else {

        next();

    }

};

Summary

In this chapter, we looked at how we can easily set up an authentication system with Keystone.js. Features such as password recovery and reset can also be easily implemented. Readers should also look at securing applications using cookies and cross-site request forgery (CSRF) protection that Keystone.js facilitates.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.