Source: controller/auth.js

'use strict';

var path = require('path'),
	capitalize = require('capitalize'),
	moment = require('moment'),
	async = require('async'),
	CoreExtension,
	CoreUtilities,
	CoreMailer,
	CoreController,
	appSettings,
	appenvironment,
	logger,
	jwtTokenSecret,
	loginExtSettings,
	oauth2serverExtSettings,
	passport,
	mongoose,
	Client,
	Token;
var jwt = require('jwt-simple');
var BasicStrategy = require('passport-http').BasicStrategy;
var BearerStrategy = require('passport-http-bearer').Strategy;

/**
 * Add two more strategies to passport for client-basic authentication, this allows you to use HTTP Basic Auth with your client token id and client secret to obtain an authorization code
 */
var configurePassport = function(){
	/**
	 * HTTP Basic Auth with client_token and client_secret
	 * @param  {string} username  username parsed from auth header
	 * @param  {string} password  password parsed from auth header
	 * @param  {function} callback) {	                        Client.findOne({ client_id: username } find client by client_token_id
	 * @return {function} callback with client from db
	 */
	passport.use('client-basic', new BasicStrategy(
	  function(username, password, callback) {
	    Client.findOne({ client_id: username }, function (err, client) {
	      if (err) { 
	      	return callback(err); 
	      }
	      else if (!client || client.client_secret !== password) { 
		      // No client found with that id or bad password
	      	return callback(null, false); 
	      }
	      else{
		      // Success
		      return callback(null, client);
	      }
	    });
	  }
	));
	/**
	 * HTTP Bearer Authentication setup using an access token
	 * @param  {string} accessToken OAUTH 2.0 token
	 * @param  {function} callback)   {	                  	var UserModelToQuery;	    Token.findOne({value: accessToken }    find token in db, to pull user account
	 * @return {function} callback with user from db
	 */
	passport.use(new BearerStrategy(
	  function(accessToken, callback) {
	  	var UserModelToQuery;
	    Token.findOne({value: accessToken }, function (err, token) {
	      if (err) { 
	      	return callback(err); 
	      }
	      else if (!token) {// No token found
	      	return callback(null, false); 
	      }
	      else{
	      	UserModelToQuery = mongoose.model(capitalize(token.user_entity_type));
		      UserModelToQuery.findOne({ _id: token.user_id }, function (err, user) {
		        if (err) { 
		        	return callback(err); 
		        }
		        else if (!user) { // No user found
		        	return callback(null, false); 
		        }
		        else{
			        // Simple example with no scope
			        callback(null, user, { scope: '*' });
		        }
		      });
	      }
	    });
	  }
	));
};

/**
 * sets additional request variables for creating new client applications, so can query the correct user collection in db
 * @param {object}   req  express request object
 * @param {object}   res  express response object
 * @param {Function} next express middleware callback function
 */
var set_client_data = function( req, res, next) {
	req.body.user_id = req.user._id;
	req.body.user_entity_type = req.user.entitytype;
	next();
};

/**
 * basic route to test authenticated request that returns user id, entitytype, username and created dates
 * @param {object}   req  express request object
 * @param {object}   res  express response object
 */
var get_user_profile = function(req, res){
 	res.send({
 		_id:req.user._id,
 		updatedat:req.user.updatedat,
 		createdat:req.user.createdat,
 		entitytype:req.user.entitytype,
 		username:req.user.username,
 	});
};

/**
 * authorization request to obtain a JWT access token (requires, username, password, clientid, entitytype (optional user entitytype))
 * @param {object}   req  express request object
 * @param {object}   res  express response object
 */
var get_jwt_token = function(req,res){
	var username = req.body.username || req.headers.username;
	var clientId = req.body.clientid || req.headers.clientid;
	var password = req.body.password || req.headers.password;
	var userQuery = {
			$or: [{
				username: {
					$regex: new RegExp(username, 'i')
				}
			}, {
				email: {
					$regex: new RegExp(username, 'i')
				}
			}]
		};
	var clientApp;
	var entitytype = req.body.entitytype || req.headers.entitytype ||'user';
	var UserModelToQuery = mongoose.model(capitalize(entitytype));
	/**
	 * saves generated expiring JWT token to user document in db
	 * @param  {object} user   user from db
	 * @param  {object} client client from db
	 * @return {object}        Promise for saving token
	 */
	var saveToken = function(user,client){
		return new Promise(function(resolve,reject){
			var expires = moment().add( oauth2serverExtSettings.jwt.expire_duration, oauth2serverExtSettings.jwt.expire_period).valueOf();	
			var jwtTokenSecret = (oauth2serverExtSettings.jwt.custom_secret)? oauth2serverExtSettings.jwt.custom_secret : appSettings.session_secret;			
			var jwt_token = jwt.encode(
				{
					iss: user._id,
					ent: user.entitytype,
					exp: expires
				}, 
				jwtTokenSecret
			);	
			var token = new Token({
	      client_id: client.client_id,
	      user_id: user._id,
	      expires: new Date(expires),
	      user_entity_type: user.entitytype,
	      value: jwt_token,
	    });

	    // Save the access token and check for errors
	    token.save(function (err) {

	      if (err) { reject(err); }
	      else{
	      	resolve({jwt_token:jwt_token,expires:expires,user:user});
	      }

	    });
		});
	};
	/**
	 * gets user from db
	 * @param  {function} resolve       promise resolve callback
	 * @param  {function} reject) 			promise reject callback
	 * @return {object}        Promise for finding db user
	 */
	var getUser = new Promise(function(resolve,reject){
		if (!username || !password){
			reject(new Error('Authentication error'));
		}
		else{
			UserModelToQuery.findOne(userQuery).select({
				'primaryasset.changes':0,
				'primaryasset.content':0,
				'assets.changes':0,
				'__v':0,
				'password':0,
				changes:0,
				content:0
			}).populate('tags categories contenttypes assets primaryasset').exec(function(err, user) {
	    	var validUserCallback = function(user){
	    		user.comparePassword(password, function(err, isMatch) {
		      	if (err) {	      		
		      		// an error has occured checking the password. For simplicity, just return a 401
							reject('Invalid Login Error');
		      	}
						if (isMatch) {	
							//clear login attempt blocks
							if (user.extensionattributes && user.extensionattributes.login && user.extensionattributes.login.attempts) {
								user.extensionattributes.login.attempts = 0;
								user.markModified('extensionattributes');
								user.save();
							}
							resolve(user);
						} 
						else {						
							// The password is wrong...
							reject('Invalid Login Authentication')
						}
					});
	  		};


				if (err ) {		
					// user cannot be found; may wish to log that fact here. For simplicity, just return a 401
					reject( new Error('Authentication error'));
				}
				else if (!user) {		
					// user cannot be found; may wish to log that fact here. For simplicity, just return a 401
					reject( new Error('Invalid Credentials'));
				}
				else if (loginExtSettings.timeout.use_limiter) {
					var limitAttemptUser = limitLoginAttempts(user);
					limitAttemptUser.save(function (err, updated) {
						if (err) {
							logger.error('Error updating user', err);
							reject(err);
						}
						else if (loginExtSettings.timeout.use_limiter && updated.extensionattributes.login.flagged) {
								loginAttemptsError(updated, function(err){
									reject(err);
								});
						}
						else {
							resolve(updated);
						}
					});
				}
				else {
					resolve(user);
				}
	    });
		}
	});
	Promise.resolve(Client.findOne({ client_id: clientId }))
		.then(function(client){
			clientApp = client;
    	if(!client){
    		throw new Error('Client not found');
    	}
    	else if(req.user){
    		return new Promise(function(resolve,reject){
    			resolve(req.user);
    		});
    	}
    	else{
    		return getUser;
    	}
		})
		.then(function(user){
  		return saveToken(user,clientApp);
		})
		.then(function(savedToken){
			// console.log('savedToken',savedToken)
			res.json({
				token : savedToken.jwt_token,
				expires : savedToken.expires,
				timeout : new Date(savedToken.expires),
				user : (typeof savedToken.user.toJSON() ==='function')?savedToken.user.toJSON():savedToken.user
			});
		})
		.catch(function(err){
			var errortosend = (appenvironment==='production')?{message:err.message}:err;
			logger.error(err);
			res.status(401).send(errortosend);
		});
};

/**
 * send error if user is locked out
 * @param  {object}   user user from db
 * @param  {function} done callback function
 * @return {function}        callback function
 */
var loginAttemptsError = function (user, done) {
	var templatepath = path.resolve(process.cwd(), loginExtSettings.timeout.view_path_relative_to_periodic);
	async.waterfall([
		function (cb) {
			var coreMailerOptions = {
				appenvironment: 'development',
				to: user.email,
				replyTo: 'Promise Financial [Do Not Reply] <no-reply@promisefin.com>',
				from: 'Promise Financial [Do Not Reply] <no-reply@promisefin.com>',
				subject: loginExtSettings.timeout.lockout_email_subject,
				emailtemplatefilepath: templatepath,
				emailtemplatedata: {
					data: user
				}
			};
			if (loginExtSettings.settings.adminbccemail || appSettings.adminbccemail) {
				coreMailerOptions.bcc = loginExtSettings.settings.adminbccemail || appSettings.adminbccemail;
			}
			CoreMailer.sendEmail(coreMailerOptions, function (err, status) {
				if (err) {
					cb(err, null);
				}
				else {
					cb(null, status);
				}
			});
		}
	], function (err, result) {
		if (err) {
			logger.error('Error sending email', err);
			return done(err);
		}
		else {
			logger.verbose('Sending account lockout email', result);
			return done(new Error('Your Account is Currently Blocked'), false, {
				message: 'Your Account is Currently Blocked'
			});
		}
	});
};

/**
 * update user to mark login attempts
 * @param  {object} user user from db
 * @return {object}      updated user
 */
var limitLoginAttempts = function (user) {
	user.extensionattributes = user.extensionattributes || {};
	if (!user.extensionattributes.login) {
		user.extensionattributes.login = {
			attempts: 0,
			timestamp: moment(),
			flagged: false,
			freezeTime: moment()
		};
	}
	user.extensionattributes.login.attempts++;
	if (!user.extensionattributes.login.flagged) {
		if (moment(user.extensionattributes.login.timestamp).isBefore(moment().subtract(loginExtSettings.timeout.attempt_interval.time, loginExtSettings.timeout.attempt_interval.unit))) {
			user.extensionattributes.login.attempts = 1;
			user.extensionattributes.login.timestamp = moment();
		}
		else if (user.extensionattributes.login.attempts >= loginExtSettings.timeout.attempts && moment(user.extensionattributes.login.timestamp).isAfter(moment().subtract(loginExtSettings.timeout.attempt_interval.time, loginExtSettings.timeout.attempt_interval.unit))) {
			user.extensionattributes.login.flagged = true;
			user.extensionattributes.login.freezeTime = moment();
		}
	}
	else {
		if (moment(user.extensionattributes.login.freezeTime).isBefore(moment().subtract(loginExtSettings.timeout.freeze_interval.time, loginExtSettings.timeout.freeze_interval.unit))) {
			user.extensionattributes.login.attempts = 1;
			user.extensionattributes.login.timestamp = moment();
			user.extensionattributes.login.flagged = false;
			user.extensionattributes.login.freezeTime = moment();
		}
	}
	user.markModified('extensionattributes');
	return user;
};


/**
 * looks up valid jwt tokens and sets user variable
 * @param {object}   req  express request object
 * @param {object}   res  express response object
 * @param {Function} next express middleware callback function
 */
var isJWTAuthenticated = function(req, res, next){
	var UserModelToQuery;
	var jwtTokenSecret = (oauth2serverExtSettings.jwt.custom_secret)? oauth2serverExtSettings.jwt.custom_secret : appSettings.session_secret;			
	
	/**
	 * Take the token from:
	 * 
	 *  - the POST value access_token
	 *  - the GET parameter access_token
	 *  - the x-access-token header
	 *    ...in that order.
	 */
	var token = (req.body && req.body.access_token) || req.query.access_token || req.headers['x-access-token'];
	// console.log('token',token);
	if (token) {

		try {
			var decoded = jwt.decode(token, jwtTokenSecret);

			if (decoded.exp <= Date.now()) {
				res.status(400).send('Access token has expired', 400);				
			}
			else{
				UserModelToQuery = mongoose.model(capitalize(decoded.ent));
				UserModelToQuery.findOne({ '_id': decoded.iss }, function(err, user){
					if (!err) {					
						req.user = user									
						return next();
					}
				})
			}
		} 
		catch (err) {			
			return next();
		}
	}
	else {
		next();
	}
}

/**
 * express middleware for ensuring either HTTP Bearer or JWT access token
 */
var checkApiAuthentication = [
	function(req,res,next){
		req.controllerData = req.controllerData || {};
		req.controllerData.skip_session = true;
		next();
	},
	isJWTAuthenticated,
	function(req,res,next){
		if(req.user){
			next();
		}
		else{
			return passport.authenticate('bearer', { session: false })(req,res,next);
		}
	}];

/**
 * oauth2server auth controller
 * @module oauth2serverController
 * @{@link https://github.com/typesettin/periodicjs.ext.oauth2server}
 * @author Yaw Joseph Etse
 * @copyright Copyright (c) 2016 Typesettin. All rights reserved.
 * @license MIT
 * @requires module:async
 * @requires module:path
 * @requires module:moment
 * @requires module:capitalize
 * @param  {object} resources variable injection from current periodic instance with references to the active logger and mongo session
 */
var controller = function (resources) {
	logger = resources.logger;
	mongoose = resources.mongoose;
	appSettings = resources.settings;
	passport = resources.app.controller.extension.login.auth.passport;
	CoreController = resources.core.controller;
	CoreUtilities = resources.core.utilities;
	CoreExtension = resources.core.extension;
	CoreMailer = resources.core.mailer;
	Client = resources.mongoose.model('Client');
	Token = resources.mongoose.model('Token');
	appenvironment = resources.settings.application.environment;
	oauth2serverExtSettings = resources.app.controller.extension.oauth2server.settings;
	loginExtSettings = resources.app.controller.extension.login.auth.loginExtSettings;
	configurePassport();
	return {
		set_client_data : set_client_data,
		isClientAuthenticated : passport.authenticate('client-basic', { session : false }),
		isBearerAuthenticated : passport.authenticate('bearer', { session: false }),
		ensureApiAuthenticated :checkApiAuthentication,
		isJWTAuthenticated: isJWTAuthenticated,
		isAuthenticated : passport.authenticate([ 'bearer'], { session: false }),
		get_user_profile: get_user_profile,
		get_jwt_token: get_jwt_token
	}
};

module.exports = controller;