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.
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.
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 ------ routes.js config ------ auth.js ------ database.js ------ passport.js views ------ index.hbs ------ layout.hbs . ------ login.hbs ------ signup.hbs ------ profile.hbs package.json server.js
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:
Page | HTTP Verb | Path | Action |
---|---|---|---|
Home | GET | / | Show the home page |
Profile | GET | /profile | Show the user profile page |
Login | GET | /login | Show the login page |
Login | POST | /login | Login user. If successful, redirect to profile page, else redirect back to login page. |
Logout | GET | /logout | Logout user and redirect to home page |
Signup | GET | /signup | Show the signup page |
Signup | POST | /signup | Create 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 ------ login.hbs ------ signup.hbs ------ profile.hbs
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"); }); }
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