Local Login strategy

Passport’s local strategy is a Node.js module that allows you to implement a username/password
authentication mechanism. Along with it, we need other modules as dependency as follow

{
  "name": "local-strategy",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "tamo",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.17.2",
    "compression": "^1.7.0",
    "ejs": "^2.5.6",
    "express": "^4.15.3",
    "express-session": "^1.15.3",
    "method-override": "*",
    "mongoose": "*:",
    "morgan": "*",
    "passport": "*"
  }
}

Run>> npm install

Step 1: Configuring Passport

To create the Passport configuration file, we have to create config folder in the root and create a new file named passport.js and express.js.  In the root folder, we have server.js or usually, we called it as app.js. Add the following code in server.js

//Set the 'NODE_ENV'  variable
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

//Load the module dependencies
const configureMongoose = require('./config/mongoose');
const configureExpress = require('./config/express');
const configurePassport = require('./config/passport');

//Create a new Mongoose Connection instance
const db = configureMongoose();

//Create a new Express application instance
const app = configureExpress();

//Configure the passport middleware
const passport = configurePassport();

app.listen(3000);
console.log('Server running at http://localhost:3000/');

module.exports = app;

 

Step 2: Register the Passport middleware

We required the Passport module and need to register the Passport middleware in your Express application and add the following code in config/express.js. The registered two middleware: the

  1. passport.initialize() middleware, which is responsible for bootstrapping the Passport module, and the
  2. passport.session() middleware, which is using the Express session to keep track of your user’s session.
const config = require('./config');
const express = require('express');
const morgan = require('morgan');
const compress = require('compression');
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
const session = require('express-session');

const flash = require('connect-flash');

const passport = require('passport');

module.exports = function() {
    const app = express();
    
    if (process.env.NODE_ENV === 'development') {
        app.use(morgan('dev'));
    } else if (process.env.NODE_ENV === 'production') {
        app.use(compress());
    }
    app.use(bodyParser.urlencoded({
        extended: true
    }));
    
    app.use(bodyParser.json());
    app.use(methodOverride());

    app.use(session({
        saveUninitialized: true,
        resave: true,
        secret: config.sessionSecret
    }));

    app.set('views', './app/views');
    app.set('view engine', 'ejs');

    app.use(passport.initialize());
    app.use(passport.session());
    
    app.use(flash());

    require('../app/routes/index.server.routes.js')(app);
    require('../app/routes/users.server.routes.js')(app);
    app.use(express.static('./public'));
    return app;
};

The passport module is installed and configure, now we need to install at least one strategy for authentication, here we are using local strategy. For local strategy we need “passport-local”  as dependency >> npm install –save passport-local

 

Step 3: Configure the local strategy

We need to configure the local strategy, we will keep the each strategy separately in folder strategies in the config folder.  To use local strategy we need to install npm package called “passport-local”. Add the local strategy code in config/strategies/local.js

// Load the module dependencies
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('mongoose').model('User');

// Create the Local strategy configuration method

module.exports = function() {
	// Use the Passport's Local strategy 

	passport.use(new LocalStrategy(function(username, password, done) {
		// Use the 'User' model 'findOne' method to find a user with the current username

		User.findOne({
			username: username
		}, (err, user) => {
			// If an error occurs continue to the next middleware

			if (err) {
				return done(err);
			}
			
			// If a user was not found, continue to the next middleware with an error message

			if (!user) {
				return done(null, false, {
					message: 'Unknown user'
				});
			}

			// If the passport is incorrect, continue to the next middleware with an error message

			if (!user.authenticate(password)) {
				return done(null, false, {
					message: 'Invalid password'
				});
			}
			
			// Otherwise, continue to the next middleware with the user object

			return done(null, user);
		});
	}));
};

For local strategy, we need above highlight code module. We register the strategy using the passport.use() method, which uses an instance of the LocalStrategy object. Notice how the LocalStrategy constructor takes a callback function as an argument. It will later call this callback when trying to authenticate a user.
The callback function accepts three arguments—username , password , and a done callback—which will be called when the authentication process is over. Inside the callback function, you will use the User Mongoose model to find a user with that username and try to authenticate it. In the event of an error, you will pass the error object to the done callback. When the user is authenticated, you will call the done callback with the user Mongoose object.

 

Step 4: Passport will handle users serialization/deserialization

Now we will create passport.js in the config folder, the passport will handle users serialization.  When a user is authenticated, Passport will save its _id property to the session. Later on, when the user object is needed, Passport will use the _id property to grab the user object from the database through deserializing. Add the following code config/passport.js

// Load the module dependencies
const passport = require('passport');
const mongoose = require('mongoose');

// Define the Passport configuration method
module.exports = function() {
	// Load the 'User' model

	const User = mongoose.model('User');
	
	// Use Passport's 'serializeUser' method to serialize the user id

	passport.serializeUser((user, done) => {
		done(null, user.id);
	});

	// Use Passport's 'deserializeUser' method to load the user document

	passport.deserializeUser((id, done) => {
		User.findOne({
			_id: id
		}, '-password -salt', (err, user) => {
			done(err, user);
		});
	});

	// Load Passport's strategies configuration files

	require('./strategies/local.js')();
};

Here along with serialization,  we are doing two important task

  1. Loading or including local strategy configuration file.
  2. Notice how we used the field options argument to make sure Mongoose doesn’t fetch the user’s password and salt properties.

 

Step 5: Adapting the User model

In the User Model, we have to define the UserSchema, adding some pre middleware, and adding some new instance methods. To do so, go to your app/models/user.js or user.server.model.js file, and add the following code

// Load the module dependencies
const mongoose = require('mongoose');
const crypto = require('crypto');
const Schema = mongoose.Schema;

// Define a new 'UserSchema'
const UserSchema = new Schema({
	firstName: String,
	lastName: String,
	email: {
		type: String,
		// Validate the email format

		match: [/.+\@.+\..+/, "Please fill a valid email address"]
	},
	username: {
		type: String,
		// Set a unique 'username' index

		unique: true,
		// Validate 'username' value existance

		required: 'Username is required',
		// Trim the 'username' field

		trim: true
	},
	password: {
		type: String,
		// Validate the 'password' value length

	validate: [
			(password) => password && password.length > 6,
			'Password should be longer'
		]
	},
	salt: {
		type: String
	},
	provider: {
		type: String,
		// Validate 'provider' value existance

		required: 'Provider is required'
	},
	providerId: String,
	providerData: {},
	created: {
		type: Date,
		// Create a default 'created' value

		default: Date.now
	}
});

// Set the 'fullname' virtual property
UserSchema.virtual('fullName').get(function() {
	return this.firstName + ' ' + this.lastName;
}).set(function(fullName) {
	const splitName = fullName.split(' ');
	this.firstName = splitName[0] || '';
	this.lastName = splitName[1] || '';
});

// Use a pre-save middleware to hash the password
UserSchema.pre('save', function(next) {
	if (this.password) {
		this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64');
		this.password = this.hashPassword(this.password);
	}

	next();
});

// Create an instance method for hashing a password
UserSchema.methods.hashPassword = function(password) {
	return crypto.pbkdf2Sync(password, this.salt, 10000, 64).toString('base64');
};

// Create an instance method for authenticating user
UserSchema.methods.authenticate = function(password) {
	return this.password === this.hashPassword(password);
};

// Find possible not used username or new unique username
UserSchema.statics.findUniqueUsername = function(username, suffix, callback) {
	// Add a 'username' suffix

	const possibleUsername = username + (suffix || '');

	// Use the 'User' model 'findOne' method to find an available unique username

	this.findOne({
		username: possibleUsername
	}, (err, user) => {
		// If an error occurs call the callback with a null value, otherwise find find an available unique username
		if (!err) {
			// If an available unique username was found call the callback method, otherwise call the 'findUniqueUsername' method again with a new suffix
			if (!user) {
				callback(possibleUsername);
			} else {
				return this.findUniqueUsername(username, (suffix || 0) + 1, callback);
			}
		} else {
			callback(null);
		}
	});
};

// Configure the 'UserSchema' to use getters and virtuals when transforming to JSON
UserSchema.set('toJSON', {
	getters: true,
	virtuals: true
});

// Create the 'User' model out of the 'UserSchema'
mongoose.model('User', UserSchema);

First, you added four fields to your UserSchema object: a salt property, which you’ll use to hash your password; a provider property, which will indicate the strategy used to register the user; a providerId property, which will indicate the user identifier for the authentication strategy; and a providerData property, which you’ll later use to store the user object retrieved from OAuth providers.

Activity done in User models

  1. We define user schema.
  2. Pre-save middleware to handle the hashing of user passwords.
  3. Create two instance methods: a hashPassword() instance method, which is used to hash a
    password string by utilizing Node.js, crypto module; and an authenticate() instance method, which accepts a string argument, hashes it, and compares it to the current user’s hashed password.
  4. findUniqueUsername() static method, which is used to find an available unique
    username for new users

 

Step 5: Add authentication views

We need to add the user signup and signin view page,  add code for app/views/signup.ejs in views.

<!DOCTYPE html>
<html>
<head>
	<!-- Use the 'title' property to render a page title  -->

	<title><%= title %></title>
</head>
<body>
  	<!-- Display flash messages  -->

	<% for(var i in messages) { %>
	   <div class="flash"><%= messages[i] %></div>
	<% } %>

	<!-- Render the signup form  -->

	<form action="/signup" method="post">
		<div>
			<label>First Name:</label>
			<input type="text" name="firstName" />
		</div>
		<div>
			<label>Last Name:</label>
			<input type="text" name="lastName" />
		</div>
		<div>
			<label>Email:</label>
			<input type="text" name="email" />
		</div>
		<div>
			<label>Username:</label>
			<input type="text" name="username" />
		</div>
		<div>	
			<label>Password:</label>
			<input type="password" name="password" />
		</div>
		<div>
			<input type="submit" value="Sign up" />
		</div>
	</form>

	<!-- Render the OAuth authentication links  -->

	<a href="/oauth/facebook">Sign in with Facebook</a>
	<a href="/oauth/twitter">Sign in with Twitter</a>
	<a href="/oauth/google">Sign in with Google</a>
</body>
</html>

Adding code for view for signin.ejs. app/views/signin.ejs

<!DOCTYPE html>
<html>
<head>
	<!-- Use the 'title' property to render a page title  -->
	<title><%= title %></title>
</head>
<body>
  	<!-- Display flash messages  -->
	<% for(var i in messages) { %>
	   <div class="flash"><%= messages[i] %></div>
	<% } %>

	<!-- Render the signin form  -->
	<form action="/signin" method="post">
		<div>
			<label>Username:</label>
			<input type="text" name="username" />
		</div>
		<div>
			<label>Password	:</label>
			<input type="password" name="password" />
		</div>
		<div>
			<input type="submit" value="Sign In" />
		</div>
	</form>

	<!-- Render the OAuth authentication links  -->
	<a href="/oauth/facebook">Sign in with Facebook</a>
	<a href="/oauth/twitter">Sign in with Twitter</a>
	<a href="/oauth/google">Sign in with Google</a>
</body>
</html>

 

Step 6: Modifying the Users controller

To alter the Users controller, go to your app/controllers/users.server.controller.js file, and
change its content as follows:

const User = require('mongoose').model('User');
const passport = require('passport');

function getErrorMessage(err) {
    let message = '';
    if (err.code) {
        switch (err.code) {
            case 11000:
            case 11001:
                message = 'Username already exists';
                break;
            default:
                message = 'Something went wrong';
        }
    } else {
        for (var errName in err.errors) {
            if (err.errors[errName].message) message = err.errors[errName].message;
        }
    }
    return message;
};

exports.renderSignin = function(req, res, next) {
    if (!req.user) {
        res.render('signin', {
        title: 'Sign-in Form',
        messages: req.flash('error') || req.flash('info')
    });
    } else {
        return res.redirect('/');
    }
};


exports.renderSignup = function(req, res, next) {
    if (!req.user) {
        res.render('signup', {
            title: 'Sign-up Form',
            messages: req.flash('error')
        });
    } else {
        return res.redirect('/');
    }
};


exports.signup = function(req, res, next) {
    if (!req.user) {
        const user = new User(req.body);
        user.provider = 'local';
        user.save((err) => {
            if (err) {
                const message = getErrorMessage(err);
                req.flash('error', message);
                return res.redirect('/signup');
            }
            req.login(user, (err) => {
                if (err) return next(err);
                    return res.redirect('/');
            });
        });
    } else {
        return res.redirect('/');
    }
};


exports.signout = function(req, res) {
    req.logout();
    res.redirect('/');
};

Here we are displaying various error message while signup and login, and also creating a new user. The signup() method uses your User model to create new users. As you can see, it first creates a user object from the HTTP request body. Then, try saving it to MongoDB. If an error occurs, the signup() method will use the getErrorMessage() method to provide the user with an appropriate error message. The getErrorMessage() method is a private method that returns a unified error message from a Mongoose error object. There are two possible errors here:

  1. MongoDB indexing error handled using the error code
  2. Mongoose validation error handled using the err.errors object.

The signout() method is also simple and uses the req.logout() method, which is provided by the Passport module to invalidate the authenticated session.

 

Step 7: Displaying flash error messages

When an authentication process is failing, it is common to redirect the request back to the signup or sign-in pages. This is done here when an error occurs, but how can your user tell what exactly went wrong? The problem is that when redirecting to another page, you cannot pass variables to that page. The solution is to use some sort of mechanism to pass temporary messages between requests. Fortunately, that mechanism already exists in the form of a node module named Connect-Flash .

The Connect-Flash module is a node module that allows you to store temporary messages in an area of the session object called flash . Messages stored on the flash object will be cleared once they are presented to the user. This architecture makes the Connect-Flash module perfect for transferring messages before redirecting the request to another page.

npm install --save connect-flash
npm install

Configuring Connect-Flash module

We have to configure the express.js file to use the flash module, we have to use require a method to include flash module and app.use() to register to our application. To do so, make the following changes in your config/express.js file:

..
.....
const flash = require('connect-flash');
...
...
app.use(flash());

Just to show the code, as we have already added to the code in step 2 in config/express.js file.

Using Connect-Flash module

Once installed, the Connect-Flash module exposes the req.flash() method, which allows you to create and retrieve flash messages. To understand it better, let’s observe the changes you’ve made to your Users controller. To understan it better at step 6, we have code renderSignup() and renderSignin() methods, which are responsible for rendering the sign-in and signup pages:

.......
messages: req.flash('error') || req.flash('info')
....
....
messages: req.flash('error')

These flash code we already added in our Step 6, Modifying the Users controller. The messages variable uses req.flash() to read the messages written to the flash. Now if you go over the signup() method, you’ll notice the following line of code:
req.flash(‘error’, message);
This is how error messages are written to the flash, again using the req.flash() method. After you learned how to use the Connect-Flash module, you might have noticed that we’re lacking a signin() method. This is because Passport provides you with an authentication method, which you can use directly in your routing definition.

Step 8: Adding routing code

Once you have your model, controller, and views configured, all that is left to do is define the user’s
routes. Add the following code in app/routes/users.server.routes.js  

const users = require('../../app/controllers/users.server.controller');
const passport = require('passport');

module.exports = function(app) {
    app.route('/signup')
    .get(users.renderSignup)
    .post(users.signup);
    app.route('/signin')
    .get(users.renderSignin)
    .post(passport.authenticate('local', {
        successRedirect: '/',
        failureRedirect: '/signin',
        failureFlash: true
    }));
    app.get('/signout', users.signout);
};

Most of the code can be self explanatory, we have to note special on POST request made to the /signin path using the passport.authenticate() method. When the passport.authenticate() method is executed, it will try to authenticate the user request using the strategy defined by its first argument. In this case, it will try to authenticate the request using the local strategy. The second parameter this method accepts is an options object, which contains three properties:

  1. successRedirect: This property tells Passport where to redirect the request once it successfully authenticated the user.
  2. failureRedirect: This property tells Passport where to redirect the request once it failed to authenticate the user
  3. failureFlash: This property tells Passport whether or not to use flash messages

Add the second routing code in app/routes/index.server.routes.js

// Load the 'index' controller
const index = require('../controllers/index.server.controller');

// Define the routes module' method
module.exports = function(app) {
	// Mount the 'index' controller's 'render' method
	app.get('/', index.render);
};

 

We also have to add code in app/controllers/index.server.controller.js 

exports.render = function(req, res) {
    res.render('index', {
        title: 'Hello World',
        userFullName: req.user ? req.user.fullName : ''
    });
};

 

Step 9: Adding view code for index, app/views/index.ejs

<!DOCTYPE html>
<html>
<head>
	<!-- Use the 'title' property to render a page title  -->

	<title><%= title %></title>
</head>
<body>
	<!-- If a user is authenticated, display the user's full name , otherwise display the authentication links -->

	<% if ( userFullName ) { %>
		<h2>Hello <%=userFullName%> </h2> 
		<a href="/signout">Sign out</a>
	<% } else { %>
		<a href="/signup">Signup</a>
		<a href="/signin">Signin</a>
	<% } %>
	<br>

	<!-- Load the static image file  -->

	<img src="img/logo.png" alt="Logo">
</body>
</html>

 

Step 10: Add the following code of configuration, we can add this code from starting of view, routes code.

Add code in config/env/development.js 

// Set the 'development' environment configuration object
module.exports = {
	db: 'mongodb://localhost/mean-development',
	sessionSecret: 'developmentSessionSecret'
};

config/env/production.js 

// Set the 'production' environment configuration object
module.exports = {
	db: 'mongodb://localhost/mean-production',
	sessionSecret: 'productionSessionSecret',
	facebook: {
		clientID: 'Facebook Application ID',
		clientSecret: 'Facebook Application Secret',
		callbackURL: 'http://localhost:3000/oauth/facebook/callback'
	},
	twitter: {
		clientID: 'Twitter Application ID',
		clientSecret: 'Twitter Application Secret',
		callbackURL: 'http://localhost:3000/oauth/twitter/callback'
	},
	google: {
		clientID: 'Google Application ID',
		clientSecret: 'Google Application Secret',
		callbackURL: 'http://localhost:3000/oauth/google/callback'
	}
};

 

config/config.js  

// Load the correct configuration file according to the 'NODE_ENV' variable
module.exports = require('./env/' + process.env.NODE_ENV + '.js');

 

config/mongoose.js 

// Load the module dependencies
const config = require('./config');
const mongoose = require('mongoose');

// Define the Mongoose configuration method
module.exports = function() {
	// Use Mongoose to connect to MongoDB
	const db = mongoose.connect(config.db);

	// Load the 'User' model 
	require('../app/models/user.server.model');

	// Return the Mongoose connection instance
	return db;
};