Reference Source

js/security/authentication_manager.js

// NPM IMPORTS
import assert from 'assert'

// COMMON IMPORTS
import T            from 'devapt-core-common/dist/js/utils/types'
import {is_browser} from 'devapt-core-common/dist/js/utils/is_browser'

let  forge = undefined
if ( is_browser() )
{
	forge = require('forge-browser').forge
} else {
	forge = require('node-forge')
}

// COMMON IMPORTS
import Credentials from 'devapt-core-common/dist/js/base/credentials'

// SERVER IMPORTS
import PluginsManager from '../plugins/plugins_manager'

// import AuthenticationPluginPassportLocalDb from './authentication_plugin_passport_local_db'
// import AuthenticationPluginPassportLocalFile from './authentication_plugin_passport_local_file'
import AuthenticationLowDbPlugin from './authentication_plugin_lowdb'


let context = 'server/security/authentication_manager'




/**
 * Authentication class to manage authentication plugins.
 * @author Luc BORIES
 * @license Apache-2.0
 */
export default class AuthenticationManager extends PluginsManager
{
	/**
	 * Create an Authentication manager class: load and create all authentication plugins.
	 * AuthenticationWrapper use created plugins.
	 * @extends PluginsManager
	 * 
	 * @param {RuntimeBase} arg_runtime - runtime.
	 * @param {string|undefined} arg_log_context - optional.
	 * @param {LoggerManager} arg_logger_manager - logger manager object (optional).
	 * 
	 * @returns {nothing}
	 */
	constructor(arg_runtime, arg_log_context, arg_logger_manager)
	{
		super(arg_runtime, arg_log_context ? arg_log_context : context, arg_logger_manager)
		
		this.is_authentication_manager = true
		
		this.authentication_is_enabled = true
		this.authentication_mode = null
	}
	
	
	
	/**
	 * Load security settings.
	 * 
	 * @param {object} arg_settings - authentication settings (Immutable object).
	 * 
	 * @returns {nothing}
	 */
	load(arg_settings)
	{
		this.enter_group('load')
		
		assert(T.isObject(arg_settings), context + ':load:bad settings object')
		assert(T.isFunction(arg_settings.has), context + ':load:bad settings immutable')
		assert(arg_settings.has('enabled'), context + ':load:bad settings.enabled')
		assert(arg_settings.has('plugins'), context + ':load:bad settings.plugins')
		assert(arg_settings.has('default_plugins'), context + ':load:bad settings.default_plugins')
		
		// LOAD AUTHENTICATION SETTINGS
		this.authentication_is_enabled = arg_settings.get('enabled')
		this.authentication_plugins = arg_settings.get('plugins')
		this.authentication_default_plugins = arg_settings.get('default_plugins')
		
		// LOAD PLUGINS
		const keys = this.authentication_plugins.toMap().keySeq().toArray()
		keys.forEach(
			(key) => {
				const plugin_cfg = this.authentication_plugins.get(key).set('name', key)
				// plugin_cfg.name = key
				const result = this.load_plugin(plugin_cfg)
				if (! result)
				{
					this.error_bad_plugin(this.authentication_mode)
				}
			}
		)
		
		
		// TODO: default security plugin ?
		// TODO: alt plugin settings ?
		
		this.leave_group('load')
	}
	
	
	
	/**
	 * Load security plugin from settings.
	 * 
	 * @param {object} arg_settings - authentication settings (Immutable object).
	 * 
	 * @returns {boolean}
	 */
	load_plugin(arg_settings)
	{
		this.enter_group('load_plugin')
		// console.log(arg_settings, context + ':load_plugin:arg_settings')
		
		const self = this
		
		assert( T.isObject(arg_settings), context + ':load_plugin:bad settings object')
		assert( T.isFunction(arg_settings.has), context + ':load_plugin:bad settings immutable')
		assert( arg_settings.has('mode'), context + ':load_plugin:bad settings.mode')
		assert( arg_settings.has('name'), context + ':load_plugin:bad settings.name')
		const plugin_name = arg_settings.get('name')
		assert( T.isNotEmptyString(plugin_name), context + ':load_plugin:bad plugin_name string')
		
		// console.log(context + ':load_plugin:plugin_name=[%s]', plugin_name)
		arg_settings = arg_settings.set('runtime', this.get_runtime() )
		arg_settings = arg_settings.set('logger_manager', this.get_logger_manager() )
		
		// LOAD PLUGIN
		const mode = arg_settings.get('mode').toLocaleLowerCase()
		switch(mode)
		{
			case 'database':
			// {
			//	 const plugin = new AuthenticationPluginPassportLocalDb(context)
			//	 this.register_plugin(plugin)
			//	 plugin.enable(arg_settings)
			//	 return true
			// }
			case 'jsonfile': {
			//	 const plugin = new AuthenticationPluginPassportLocalFile(context)
			//	 this.register_plugin(plugin)
			//	 plugin.enable(arg_settings)
			//	 return true
			// }
			// default: {
				// TODO NAMe
				const plugin = new AuthenticationLowDbPlugin(this, plugin_name, context)
				// self.info(context + ':load_plugin:create plugin for mode [' + mode + '] for name [' + plugin.get_name() + ']')
				
				this.register_plugin(plugin)
				.then(
					(result) => {
						if (result)
						{
							plugin.enable(arg_settings)
							self.info(context + ':load_plugin:success for mode [' + mode + '] for name [' + plugin.get_name() + ']')
						}
						else
						{
							self.error(context + ':load_plugin:failure for mode [' + mode + '] for name [' + plugin.get_name() + ']')
						}
					}
				)
				.catch(
					(reason) => {
						self.error(context + ':load_plugin:failure for mode [' + mode + '] for name [' + plugin.get_name() + ']:' + reason)
					}
				)
				
				this.leave_group('load_plugin:jsonfile or database')
				return true
			}
			case 'token': {
				this.leave_group('load_plugin:token')
				return true // TODO: plugin auth token
			}
		}
		
		this.leave_group('load_plugin:error')
		return false
	}
	
	
	
	/**
	 * Authenticate a user with giving credentials.
	 * 
	 * @param {Credentials} arg_credentials - credentials object.
	 * 
	 * @returns {Promise} - a promise of boolean.
	 */
	authenticate(arg_credentials)
	{
		this.enter_group('authenticate')
		
		// console.log(context + ':authenticate:arg_credentials', arg_credentials)
		
		let all_promises = []
		this.registered_plugins.forEach(
			(plugin) => {
				const promise = plugin.authenticate(arg_credentials)
				all_promises.push(promise)
			}
		)
		
		const promise = Promise.all(all_promises).then(
			(promise_results) => {
				for(let result of promise_results)
				{
					if (result)
					{
						return true
					}
				}
				return false
			}
		)
		
		this.leave_group('authenticate')
		return promise
	}
	
	
	
	/**
	 * Hash a password.
	 * 
	 * @param {string} arg_password - password to hash.
	 * @param {string|undefined} arg_digest_method - digest method name (sha1,sha256,sha384,sha512,md5)
	 * @param {string|undefined} arg_encoding_method - encoding method name (hex,utf8,utf16,binary,base64,hexstr)
	 * 
	 * @returns {string} - hashed password
	 */
	hash_password(arg_password, arg_digest_method, arg_encoding_method)
	{
		assert(T.isString(arg_password), context + ':bad password string')
		arg_digest_method = T.isString(arg_digest_method) ? arg_digest_method : 'sha1'
		arg_encoding_method = T.isString(arg_encoding_method) ? arg_encoding_method : 'hex'
		
		// GET MESSAGE DIGEST FUNCTION
		let md = null
		switch(arg_digest_method.toLocaleLowerCase())
		{
			case 'sha1':   md = forge.md.sha1.create(); break
			case 'sha256': md = forge.md.sha256.create(); break
			case 'sha384': md = forge.md.sha384.create(); break
			case 'sha512': md = forge.md.sha512.create(); break
			case 'md5':	md = forge.md.md5.create(); break
			default: this.error_bad_digest_method(arg_digest_method); return null
		}
		assert(md, context + ':bad message digest object')
		md.update(arg_password)
		
		// GET ENCODED MESSAGE
		let encoded = null
		switch(arg_encoding_method.toLocaleLowerCase())
		{
			case 'hex':
				encoded = md.digest().toHex(); break
			case 'utf8':
			case 'utf-8':
				encoded = md.digest().toString('utf8'); break
			case 'utf16':
			case 'utf-16':
				encoded = md.digest().toString('utf16'); break
			case 'binary':
				encoded = md.digest().toString('binary'); break
			case 'base64':
				encoded = md.digest().toString('base64'); break
			case 'hexstr':
				encoded = md.digest().toString('hex'); break
			default: this.error_bad_encoding_method(arg_encoding_method); return null
		}
		assert(encoded, context + ':bad message encoding result')
		
		return encoded
	}
	
	
	
	/**
	 * Check request credentials authentication.
	 *	Request format:
	 *		req.username=...
	 *		req.authorization={
	 *			scheme: <Basic|Signature|...>,
	 *			credentials: <Undecoded value of header>,
	 *			basic: {
	 *				username: $user
	 *				password: $password
	 *			}
	 *		}
	 *
	 * @param {object} arg_request - request object.
	 * 
	 * @returns {boolean}
	 */
	check_request_authentication(arg_request)
	{
		if ( !this.authentication_is_enabled )
		{
			return true
		}
		try{
			const credentials = this.get_credentials(arg_request).get_credentials()
			
			// DEBUG
			// debugger
			
			if (arg_request.is_authenticated && credentials.user_name && credentials.user_pass_digest)
			{
				return true
			}
		} catch(e)
		{
			this.error(context + ':check_request_authentication:exception', e)
		}
		return false
	}
	
	
	
	/**
	 * Get request credentials from headers.
	 *	Request format:
	 *		req.username=...
	 *		req.authorization={
	 *			scheme: <Basic|Signature|...>,
	 *			credentials: <Undecoded value of header>,
	 *			basic: {
	 *				username: $user
	 *				password: $password
	 *			}
	 *		}
	 * 
	 * @param {object} arg_request - request object.
	 * 
	 * @returns {Credentials|undefined} - credentials instance.
	 */
	get_credentials(arg_request)
	{
		this.info('get_credentials')
		
		// DEBUG
		// console.log(arg_request.url, 'arg_request.url')
		// console.log(arg_request.queries, 'arg_request.queries')
		// console.log(arg_request.password, 'arg_request')
		console.log(arg_request.query, 'get_credentials(req):arg_request.query')
		// console.log(arg_request.params, 'arg_request.params')
		
		
		// CHECK REQUEST
		// let credentials = { 'user_name':null, 'password':null, 'token':null, 'expire':null }
		if (!arg_request)
		{
			return undefined
		}


		// DEFINE EMPTY CREDENTIALS
		let credentials = Credentials.get_empty_credentials()
		
		credentials.tenant = 'tenantA' // TODO
		credentials.env = 'dev' // TODO
		credentials.application = 'devtools' // TODO

		credentials.token = '' // TODO

		credentials.ts_login = Date.now()
		credentials.ts_expiration = Date.now() + 999999999 // TODO
		
		
		// REQUEST ALREADY HAVE PROCESSED CREDENTIAL
		if ( T.isObject(arg_request.devapt_credentials) && arg_request.devapt_credentials.is_credentials )
		{
			this.info('get_credentials:authentication from cache')
			return arg_request.devapt_credentials
		}
		
		
		// EXPRESS REQUEST
		if (arg_request && arg_request.query && arg_request.query.username && arg_request.query.password)
		{
			this.info('get_credentials:authentication with query map')
			
			credentials.user_name = arg_request.query.username
			credentials.user_pass_digest = arg_request.query.password
			arg_request.devapt_credentials = new Credentials(credentials)
			
			return arg_request.devapt_credentials
		}
		
		
		// RESTIFY WITH QUERY STRING
		let query_str = null
		if (arg_request && T.isString(arg_request.query) )
		{
			this.info('get_credentials:authentication with query string')
			query_str = arg_request.query
		}
		if (arg_request && T.isFunction(arg_request.query) )
		{
			this.info('get_credentials:authentication with query function')
			query_str = arg_request.query()
		}
		if ( T.isString(query_str) )
		{
			const queries = query_str.split('&')
			// console.log(queries, 'queries part')
			
			queries.forEach(
				(item/*, index, arr*/) => {
					const parts = item.split('=')
					if ( T.isArray(parts) && parts.length == 2 )
					{
						const key = parts[0]
						const value = parts[1]
						if (key == 'username')
						{
							credentials.user_name = value
							return
						}
						if (key == 'password')
						{
							credentials.user_pass_digest = value
							return
						}
					}
				}
			)
			
			if (credentials.user_name && credentials.user_pass_digest)
			{
				arg_request.devapt_credentials = new Credentials(credentials)
				return arg_request.devapt_credentials
			}
		}
		
		
		// RESTIFY QUERY PARSER PLUGIN (NOT WORKING YET)
		if (arg_request && arg_request.params && arg_request.params.username && arg_request.params.password)
		{
			this.info('get_credentials:authentication with params args')
			
			credentials.user_name = arg_request.params.username
			credentials.user_pass_digest = arg_request.params.password
			arg_request.devapt_credentials = new Credentials(credentials)
			return arg_request.devapt_credentials
		}
		
		
		// RESTIFY AUTHORIZATION PLUGIN
		if (arg_request && arg_request.authorization)
		{
			this.info('get_credentials:authentication with basic auth header args')
			
			if (!arg_request.authorization.basic)
			{
				return credentials
			}
			
			credentials.user_name = arg_request.authorization.basic.username
			credentials.user_pass_digest = arg_request.authorization.basic.password
			arg_request.devapt_credentials = new Credentials(credentials)
			return arg_request.devapt_credentials
		}
		
		this.error_bad_credentials_format()
		
		console.log(arg_request.url, 'arg_request.url')
		console.log(arg_request.queries, 'arg_request.queries')
		console.log(arg_request.password, 'arg_request')
		console.log( T.isFunction(arg_request.query) ? arg_request.query() : arg_request.query, 'arg_request.query')
		console.log(arg_request.params, 'arg_request.params')

		return undefined
	}
	
	
	
	/**
	 * Error wrapper - error during plugin loading.
	 * 
	 * @param {string} arg_plugin_mode - plugin mode.
	 * 
	 * @returns {nothing}
	 */
	error_bad_plugin(arg_plugin_mode)
	{
		this.error('bad plugin [' + arg_plugin_mode + ']')
	}
	
	
	
	/**
	 * Error wrapper - unknow digest method.
	 * 
	 * @param {string} arg_digest_method - digest method name.
	 * 
	 * @returns {nothing}
	 */
	error_bad_digest_method(arg_digest_method)
	{
		this.error('bad digest method [' + arg_digest_method + ']')
	}
	
	
	
	/**
	 * Error wrapper - unknow encoding method.
	 * 
	 * @param {string} arg_encoding_method - encoding method name.
	 * 
	 * @returns {nothing}
	 */
	error_bad_encoding_method(arg_encoding_method)
	{
		this.error('bad encoding method [' + arg_encoding_method + ']')
	}
	
	
	/**
	 * Error wrapper - unknow request credentials format.
	 * 
	 * @returns {nothing}
	 */
	error_bad_credentials_format()
	{
		this.error('bad request credentials format')
	}
}