DAT602 – Face Login and User Authentication with Node.js Passport

In my previous post, I described the development of a web interface that used Javascript to access Microsoft’s Cognitive Services Azure Face API. This interface allowed an image of a face to be captured via a web-cam and identified against a previously trained Person Group.

Now that this basic system is functioning as expected, I can use it as the basis for a Node.js/Express user authentication system using the popular Node package, Passport.

Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

(Passport, no date)

How Face Login will Work

Currently, Python scripts are used to do the work of creating and training the face recognition system which is accessible via Face API calls. These scripts also assign a name to each person trained. The face login system will replace the standard username/password web form by providing a web-cam preview and a login button.

Face Login

I will be keeping the standard username/password login form but, behind the scenes, Javascript will be used to hide the form, populate both the username and password fields using the person’s name as identified via the Python scripts, and finally submit the form.

Face Login Web Form

The identified user’s name/password will be authenticated against the details held in a Mongo database. If they match, the user will be taken to a profile page showing some of their account details.

Application Structure

- app
------ models
---------- user.js  <!-- the user model -->
------ routes.js    <!-- application routes -->
- config
------ auth.js      <!-- social media API details -->
------ database.js  <!-- database connection settings -->
------ passport.js  <!-- configuration for Passport -->
- views
------ index.hbs    <!-- home page -->
------ layout.hbs . <!-- layout -->
------ login.hbs    <!-- face login page -->
------ signup.hbs   <!-- user signup page -->
------ profile.hbs  <!-- user profile page -->
- package.json      <!-- packages -->
- server.js         <!-- main application file -->

The main file which runs the applications is the server.js file. It pulls in all the required modules and configuration files:

// require modules
var express  = require('express');
var app      = express();
var port     = process.env.PORT || 8080;
var mongoose = require('mongoose');
var passport = require('passport');
var flash    = require('connect-flash');

var morgan       = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser   = require('body-parser');
var session      = require('express-session');

// load config variables
var configDB = require('./config/database');
var config = require('./config/auth');

// connect to database
// mongoose.connect(configDB.url); (deprecated)
var promise = mongoose.connect(configDB.url, {
  useMongoClient: true,
});

require('./config/passport')(passport); // pass passport for configuration

// set up express application
app.use(morgan('dev')); // log every request to the console
app.use(cookieParser()); // read cookies (needed for auth)
app.use(bodyParser.json()); // get information from html forms
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('public')) // serve static files

// Use Handlebars view engine
app.set('view engine', 'hbs');

// required for passport
app.use(session({
    secret: 'dat602-secret', // session secret
    resave: true,
    saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session()); // persistent login sessions
app.use(flash()); // use flash messaging

// routes
require('./app/routes.js')(app, passport, config); // load our routes and pass in our app and fully configured passport

// start app
app.listen(port);
console.log('Connected on port ' + port);

The routes.js file will handle requests for the application’s pages. The pages it needs to handle are:

PageHTTP VerbPathAction
HomeGET/Show the home page
ProfileGET/profileShow the user profile page
LoginGET/loginShow the login page
LoginPOST/loginLogin user. If successful, redirect to profile page, else redirect back to login page.
LogoutGET/logoutLogout user and redirect to home page
SignupGET/signupShow the signup page
SignupPOST/signupCreate a user. If successful, redirect to profile page, else redirect back to signup page.

The code for routes.js:

module.exports = function(app, passport) {

    // home
    app.get('/', function(req, res) {
        res.render('index.hbs', {
            user: req.user
        });
    });

    // profile
    app.get('/profile', isLoggedIn, function(req, res) {
        res.render('profile.hbs', {
            user : req.user
        });
    });

    // logout
    app.get('/logout', function(req, res) {
        req.logout();
        res.redirect('/');
    });

    // show the login form
    app.get('/login', function(req, res) {
        res.render('login.hbs', { message: req.flash('loginMessage') });
    });

    // process the login form
    app.post('/login', passport.authenticate('local-login', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/login', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));

    // show the signup form
    app.get('/signup', function(req, res) {
        res.render('signup.hbs', { message: req.flash('signupMessage') });
    });

    // process the signup form
    app.post('/signup', passport.authenticate('local-signup', {
        successRedirect : '/profile', // redirect to the secure profile section
        failureRedirect : '/signup', // redirect back to the signup page if there is an error
        failureFlash : true // allow flash messages
    }));
};

// route middleware to ensure user is logged in
function isLoggedIn(req, res, next) {
    if (req.isAuthenticated())
        return next();

    res.redirect('/');
}

The profile page is only accessible to logged in users, so route middleware is used.

// profile
    app.get('/profile', isLoggedIn, function(req, res) {
        res.render('profile.hbs', {
            user : req.user
        });
    });

The isLoggedIn function checks to see if the user accessing this route is logged and, if not, redirects them to the home page.

Views

For a user authentication system, the views are quite simple:

- views
------ index.hbs    <!-- home page -->
------ login.hbs    <!-- face login page -->
------ signup.hbs   <!-- user signup page -->
------ profile.hbs  <!-- user profile page -->

I have made use of the Handlebars view engine, which allows layout templates to be created, providing an easily maintainable and consistent site layout.

The layout.hbs file is the master page layout. It includes meta information, Bootstrap, Font Awesome, and JQuery via CDNs, and some conditional logic for displaying login/logout links, etc.

<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
        <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css">
        <style>
            .starter-template {
                padding: 5rem 1.5rem;
            }
        </style>
        <title>Face Login</title>
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
                <a class="navbar-brand" href="/"><i class="far fa-grin-wink"></i> Face Login</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                    <ul class="navbar-nav mr-auto">
                        {{#unless user}}
                        <li class="nav-item">
                            <a class="nav-link" href="/login">Login</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="/signup">Signup</a>
                        </li>
                        {{/unless}}
                        {{#if user}}
                            <li class="nav-item">
                                <a class="nav-link" href="/profile">Profile</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="/logout">Logout</a>
                            </li>
                        {{/if}}
                    </ul>
                </div>
            </nav>
        </header>
        <main role="main" class="container">
            <div class="starter-template">
                {{#if message}}
                    <div class="alert alert-danger">{{message}}</div>
                {{/if}}
                {{{ body }}}
            </div>
        </main>
        <footer class="footer">
            <div class="container">
                <span class="text-muted"><i class="far fa-grin-wink"></i> DAT602 - Face Login</span>
            </div>
        </footer>
        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
    </body>
</html>

The use of the {{{body}}} Handlebars code ensures that content from our views will be injected into the layout.hbs file in the appropriate place.

The code for index.hbs (Home page):

<h1><i class="far fa-grin-wink mb-4"></i> Face Login</h1>

{{#unless user}}
  <div class="row text-center">
      <div class="col text-center my-auto">
          <div class="card bg-secondary">
              <div class="card-body align-items-center d-flex justify-content-center">
                  <a href="/login" class="btn btn-lg btn-block btn-secondary">Login</a>
              </div>
          </div>
      </div>
      <div class="col text-center my-auto">
          <div class="card bg-secondary">
              <div class="card-body align-items-center d-flex justify-content-center">
                  <a href="/signup" class="btn btn-lg btn-block btn-secondary">Signup</a>
              </div>
          </div>
      </div>
  </div>
{{/unless}}

The code for login.hbs (Login page):

<style>
    /* Hide login form elements */
    .form-group  {
        display: none; 
    }
    /* Preview styling */
    #viewport, #my_camera {
        border: 1px solid silver;
        background-color: white;
    }
    #viewport {
        background: white url('camera-solid.svg') no-repeat center;
        background-size: 100px 100px;
    }
</style>
<h1><i class="fas fa-sign-in-alt"></i> Login</h1>
<div class="row text-center">
    <div class="col text-center my-auto">
        <div class="card bg-light">
            <div class="card-body align-items-center d-flex justify-content-center">
                <div id="my_camera"></div>
            </div>
        </div>
    </div>
    <div class="col text-center my-auto">
        <div id="card" class="card bg-warning">
            <div class="card-body align-items-center d-flex justify-content-center">
                <canvas id="viewport" width="320" height="240"></canvas>
            </div>
        </div>
    </div>
</div>

<!-- Login Form -->
<form id="login" action="/login" method="post">
    <div class="form-group">
        <label>Username</label>
        <input id="name" type="text" class="form-control" name="username">
    </div>
    <div class="form-group">
        <label>Password</label>
        <input id="password" type="password" class="form-control" name="password">
    </div>
    <input type=button class="btn btn-primary btn-lg mt-2" value="Login" onClick="take_snapshot()">
</form>
<script src="webcam.min.js"></script>
<script src="javascript.js"></script>

The login.hbs file includes two Javascript files which provide the functionality for the web-cam preview and the Face API calls. The javascript.js file was used in my previous post and has been amended slightly to automatically populate the login form’s username and password fields and submit the form:

function getName(personIdGlobal) {
    var params = {
          'personGroupId': 'users',
          'personId': personIdGlobal
      };

      $.get({
            url: "https://westus.api.cognitive.microsoft.com/face/v1.0/persongroups/users/persons/" + personIdGlobal,
            headers: {
              'Ocp-Apim-Subscription-Key': 'XXXXX'
            },
      })
      .done(function(data) {
          $("#name").val(data.name);
          $("#password").val(data.name);
          $('form#login').submit();
      })
      .fail(function() {
          alert("error");
      });
  }

The code for profile.hbs (Profile page):

<h1 class="mb-4">Profile Page</h1>
<div class="card">
    <h5 class="card-header">{{user.local.username}}</h5>
    <div class="card-body">
        <p class="card-text">ID: {{user.id}}</p>
        <p class="card-text">Password: {{user.local.password}}</p>
    </div>
</div>

The code for signup.hbs (Signup page):

<h1><i class="fas fa-user-plus mb-4"></i> Signup</h1>
<!-- Signup Form -->
<form action="/signup" method="post">
  <div class="input-group mb-4">
    <div class="input-group-prepend">
      <div class="input-group-text"><i class="fas fa-user"></i></div>
    </div>
    <input type="text" class="form-control form-control-lg" placeholder="Username" name="username">
  </div>
  <div class="input-group mb-4">
    <div class="input-group-prepend">
      <div class="input-group-text"><i class="fas fa-lock"></i></div>
    </div>
    <input type="password" class="form-control form-control-lg" placeholder="Password" name="password">
  </div>
  
  <button type="submit" class="btn btn-secondary btn-lg">Signup</button>
</form>

User Authentication with Passport

Each user will have their username and password stored in a Mongo database. A user model will be required. This model defines the schema that will be used to store the information, as well as a number of methods related to users, such as hashing their password.

The code for the user model looks like:

// require modules
var mongoose = require('mongoose');
var bcrypt   = require('bcrypt-nodejs');

// define the schema for our user model
var userSchema = mongoose.Schema({

    local            : {
        username     : String,
        password     : String
    }
});

// method to generate a hash
userSchema.methods.generateHash = function(password) {
    return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
};

// method to check if password is valid
userSchema.methods.validPassword = function(password) {
    return bcrypt.compareSync(password, this.local.password);
};

// create the model for users and make it available to our app
module.exports = mongoose.model('User', userSchema);

The passport.js file handles the login and signup functionality:

// require an authentication strategy
var LocalStrategy = require('passport-local').Strategy;

// require the user model
var User = require('../app/models/user');

module.exports = function(passport) {

    // serialize the user for the session
    passport.serializeUser(function(user, done) {
        done(null, user.id);
    });

    // deserialize the user
    passport.deserializeUser(function(id, done) {
        User.findById(id, function(err, user) {
            done(err, user);
        });
    });

    // login
    passport.use('local-login', new LocalStrategy({
        usernameField : 'username',
        passwordField : 'password',
        passReqToCallback : true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
    },
    function(req, username, password, done) {
        // asynchronous
        process.nextTick(function() {
            User.findOne({ 'local.username' :  username }, function(err, user) {
                // if there are any errors, return the error
                if (err)
                    return done(err);

                // if no user is found, return the message
                if (!user)
                    return done(null, false, req.flash('loginMessage', 'No user found.'));

                if (!user.validPassword(password))
                    return done(null, false, req.flash('loginMessage', 'Incorrect password.'));

                // no errors, return user
                else
                    return done(null, user);
            });
        });

    }));

    // signup
    passport.use('local-signup', new LocalStrategy({
        usernameField : 'username',
        passwordField : 'password',
        passReqToCallback : true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
    },
    function(req, username, password, done) {
        // asynchronous
        process.nextTick(function() {
            // if the user is not already logged in:
            if (!req.user) {
                User.findOne({ 'local.username' :  username }, function(err, user) {
                    // if there are any errors, return the error
                    if (err)
                        return done(err);

                    // check to see if theres already a user with that username
                    if (user) {
                        return done(null, false, req.flash('signupMessage', 'That username is already taken.'));
                    } else {

                        // create the user
                        var newUser            = new User();
                        newUser.local.username = username;
                        newUser.local.password = newUser.generateHash(password);
                        newUser.save(function(err) {
                            if (err)
                                return done(err);
                            return done(null, newUser);
                        });
                    }
                });
            } 
        });
    }));
};

Bibliography

Handlebars (no date) Handlebars. Available at: https://handlebarsjs.com/ (Accessed: 22 November 2018).

Passport (no date) Passport. Available at: http://www.passportjs.org (Accessed: 22 November 2018).

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.