Basic Authentication on Node.js – Express and Mongoose

via Axiom Zen

Hello!

This post is about the most important parts I can think of implementing signup and login to Node.js powered by Mongoose and Express.js.

This post have a huge audience, but guys, please keep in mind that this post was done a while ago. It works pretty well as a cook-book, but there probably are better options for achieving this nowadays, new npm packages and such.

Implementing last night, I was impressed on how fast had the functional part done (around 3 hours), pretty much the same time I’d had it done in Rails (if not faster).

Goal:

  • 3 urls: GET login & signup, POST login, POST signup.
  • User fields: email, nick, password(encrypted).
  • Validate (and show) messages for all fields.
I’ve setup a small Github project that may be used as example (assuming you have mongoDB installed and running in localhost) – DOWNLOAD IT FROM GITHUB  

Model: user.js

var Schema = mongoose.Schema
  , ObjectId = Schema.ObjectId
  , Validations = require('./validations.js')
  , salt = 'mySaltyString'
  , SHA2 = new (require('jshashes').SHA512)()

function encodePassword( pass ){
	if( typeof pass === 'string' && pass.length < 6 ) return ''

	return SHA2.b64_hmac(pass, salt )
}

var UserSchema = new Schema({
    nick        : {type: String, required: true, unique: true, trim: true }
  , email       : {type: String, required: true, unique: true, trim: true, lowercase: true }
  , password    : {type: String, set: encodePassword, required: true }
});

UserSchema.statics.classicLogin = function(login, pass, cb) {
	if( login && pass ){
		mongoose.models.User
			.where( 'email', login )
			.where( 'password', encodePassword(pass) )
	  	.findOne( cb )
	} else {
		// just to launch the standard error
		var o = new this({nick: 'VeryUniquejerewelA', password: '', email: login+'aaa'})
		o.save(cb)
	}
}
UserSchema.path('nick').validate( Validations.uniqueFieldInsensitive('User', 'nick' ), 'unique' )
UserSchema.path('email').validate( Validations.uniqueFieldInsensitive('User', 'email' ), 'unique' )
UserSchema.path('email').validate( Validations.emailFormat, 'format' )
UserSchema.path('password').validate( Validations.cannotBeEmpty, 'password' )
UserSchema.plugin( mongoose.availablePlugins.timestamper )

mongoose.model('User', UserSchema)

.Highlights of this code: We are using the package jshashes, which supplies many convenient encryption methods, among those SHA512 –strong enough
It is a good practice to use a salt along, represented by the var salt. In practice it makes way difficult for a cracker that acquired access to the database do decipher the passwords stored.
The method encodePassword is used at two occasions, when setting the User password, and when retrieving it from database.
UserSchema.statics is a object that stores additional static methods our User model will offer.
The function classicLogin requires both login and pass to search the db for existence, otherwise, it will launch an error (kinda of a smelly workaround to make it work dry )
UserSchema.path(…).validate offers us validations, in our case, we do not allow repeated email or nick, and password should be bigger at least 6 characters long. Also email should at least look like a email.
All those validations work along with the own Schema definition: required, unique, trim, lowercase

Route: auth.js

// app.get( '/auth/popover', auth.popover);
exports.popover = function(req, res){
	//req.session.popover = new Date()
	console.log('My session:', req.session)
  res.render('auth/index_pop', req.viewVars);
};

// CLASSIC LOGIN / SIGNUP       --because everyauth seems too messy for login+pass
// app.post('/auth/classic-signup', auth.classicSignup)
exports.classicSignup = function(req,res,next) {
	if( !req.body ){
		console.log('why u signup nobody?')
		return res.redirect('/?nobodySignup')
	}

	var user = new app.models.User()

	user.set('nick', req.body.nick)
	user.set('email', req.body.email)
	user.set('password', req.body.pass)
	user.set('providers', ['signup:'+user.get('email')])
	user.set('profiles', [{ _name: 'signup'}])

	user.save( function(err) {
		if( err ){ // validation failed

			req.viewVars.u = user
			return classicYieldErr( req, res, 'signUp', err)

		} else { // signup successful

			req.session.user = {
				provider: 'signup',
				id: user.get('id'),
				nick: user.get('nick'),
			}

			req.flash('notice', 'Welcome!')
			req.viewVars.welcome_login = "Welcome, "+user.nick

  		res.render('auth/win_pop', req.viewVars )
		}
	})
};

// app.post('/auth/classic-login',  auth.classicLogin)
exports.classicLogin = function(req,res,next) {
	if( !req.body ){
		console.log('why u login nobody?')
		return res.redirect('/?nobodyLogin')
	}

	app.models.User.classicLogin( req.body.email, req.body.pass, function(err, user) {
		if( err ){ // validation failed

			return classicYieldErr( req, res, 'signIn', err)

		} else {

			if( user ){ // login

				req.session.user = {
					provider: 'signup',
					id: user.get('id'),
					nick: user.get('nick'),
				}

				req.flash('notice', 'Welcome!')
				req.viewVars.welcome_login = "Welcome, "+user.nick

	  		res.render('auth/win_pop', req.viewVars )

			} else { // not found
				return classicYieldErr( req, res, 'signIn', {errors:
					{'loginpass': {
						name: 'V',
						path: 'login+password',
						type: 'loginpass'
					}
				}})
			}

		}
	})
};

// display form error
function classicYieldErr( req, res, mode, err ){
	req.viewVars.erroredForm = mode
	if( mode === 'signIn' ){
		req.viewVars.signin_errors = app.helpers.displayErrors( err )
	} else {
		req.viewVars.signup_errors = app.helpers.displayErrors( err )
	}
	req.viewVars.email = req.body.email

	res.render('auth/index_pop', req.viewVars);
}

Here we define the 3 routes;
– one route(GET) that defines one page for both login or signup
– one route(POST) to submit login
– one route(POST) to submit signup
About the last 2, their only role is to allow access for valid data. That is, valid signup data, or login+password existing in our collection from MongoDB.
The function classicYieldErr is there just to serve the errors in sort of an uniform way. We will see about this function on the next file

Model Helper: validations.js

exports.uniqueFieldInsensitive =  function ( modelName, field ){
	return function(val, cb){
		if( val && val.length ){ // if string not empty/null
			// only for new docs
			if( this.isNew ){
				mongoose.models[modelName].where(
					field, new RegExp('^'+val+'$', 'i')
				).count(function(err,n){
					// false when validation fails
					cb( n < 1 )
				})
			} else {
				cb( true )
			}
		} else { // raise error of unique if empty // may be confusing, but is rightful
			cb( false )
		}
	}
}

exports.emailFormat = function( val ){
	// false when validation fails
	return (/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i).test( val )
}

exports.cannotBeEmpty = function( val ){
	console.log('pass val is:', val)

	// not all passwords should be set, BUT when string, should be encoded
	if( typeof val === 'string' ){
		if( val.length ){ // when it comes empty, something went wrong!
			return true
		} else {
			return false
		}
	} else {
		return false
	}
}

This file contains some very important function, being intuitive, I will only comment on the first.
uniqueFieldInsensitive is actually a validation creator. It uses JS’s closure ability along with the powerful this object to wrap the context where this will represent whichever Model is calling it.
The intent of the function is to search the db for any repeated occurrence of the field, case-insensitive. This way the system will block a user “John” from signinup, if “john” is present.

Helper: form_helper.js


//app.helpers.displayErrors = require('./helpers/form_helper.js').displayErrors

stripErrors = function(mongooseErr){
	var prop, list = [];
	for( prop in mongooseErr.errors ){
		list.push( [mongooseErr.errors[prop].path, mongooseErr.errors[prop].type] )
	}
	return list
}

/*
 * Translate mongoose errors into a <li> of errors
 */
exports.displayErrors = function( mongooseErr ){

	console.log( 'mongoose errs', mongooseErr )

	var list = stripErrors( mongooseErr )

	var output = []
	list.forEach(function(e,i){
		switch( e[1] ){
			case( 'unique' ):
				output.push( e[0]+" is taken" )
				break;
			case( 'required' ):
				output.push( e[0]+" is "+e[1] )
				break;
			case( 'format' ):
				output.push( e[0]+" has a bad format" )
				break;
			case( 'password' ):
				output.push( "password should be at least 6 char long" )
				break;
			case( 'loginpass' ):
				output.push( "login+password not found" )
				break;

			default:
				output.push( e[0]+": "+e[1] )
				break;
		}
	})
	if( output.length ){
		// condense all items in an error list
		output = [output.join( '</li><li>\n' )]
		output.unshift( '<ul><li>' )
		output.push( '</li><ul>' )

		// wrap in a div
		output.unshift( "<div class='error block'>" )
		output.push('</div>')
	}
	return output.join('\n')
}

This file provides a translation from Mongoose errors to user-readable errors. In order to offer a good UX, it is important to give accurate feedback. Notice this file could benefit from some refinement 🙂

Don’t forget to download the code from github, it plays well along everyauth module 🙂

Cheers!

Advertisements