@themost/web
Version:
MOST Web Framework 2.0 - Web Server Module
507 lines (483 loc) • 17.5 kB
JavaScript
/**
* @license
* MOST Web Framework 2.0 Codename Blueshift
* Copyright (c) 2017, THEMOST LP All rights reserved
*
* Use of this source code is governed by an BSD-3-Clause license that can be
* found in the LICENSE file at https://themost.io/license
*/
var TraceUtils = require('@themost/common/utils').TraceUtils;
var RandomUtils = require('@themost/common/utils').RandomUtils;
var AbstractClassError = require('@themost/common/errors').AbstractClassError;
var AbstractMethodError = require('@themost/common/errors').AbstractMethodError;
var HttpUnauthorizedError = require('@themost/common/errors').HttpUnauthorizedError;
var HttpForbiddenError = require('@themost/common/errors').HttpForbiddenError;
var LangUtils = require('@themost/common/utils').LangUtils;
var Args = require('@themost/common/utils').Args;
var HttpApplicationService = require('./../types').HttpApplicationService;
var Symbol = require('symbol');
var _ = require('lodash');
var moment = require('moment');
var crypto = require('crypto');
var Q = require('q');
var optionsProperty = Symbol('options');
/**
* @class
* @constructor
* @implements AuthenticateRequestHandler
* @implements PreExecuteResultHandler
*/
function AuthHandler() {
//
}
/**
* @param {IncomingMessage|ClientRequest} request
* @returns {*}
*/
AuthHandler.parseCookies = function(request) {
var list = {},
rc = request.headers.cookie;
rc && rc.split(';').forEach(function( cookie ) {
var parts = cookie.split('=');
list[parts.shift().trim()] = unescape(parts.join('='));
});
return list;
};
AuthHandler.ANONYMOUS_IDENTITY = { name: 'anonymous', authenticationType:'None' };
/**
* Authenticates an HTTP request and sets user or anonymous identity.
* @param {HttpContext} context
* @param {Function} callback
*/
AuthHandler.prototype.authenticateRequest = function (context, callback) {
try {
callback = callback || function() {};
var cookies = {}, model = context.model('User');
var config = context.getApplication().getConfiguration();
var settings = config.settings ? (config.settings.auth || { }) : { } ;
settings.name = settings.name || '.MAUTH';
if (context && context.request)
cookies = AuthHandler.parseCookies(context.request);
if (cookies[settings.name]) {
var str = null;
try {
str =context.getApplication().getEncryptionStrategy().decrypt(cookies[settings.name]);
}
catch (err) {
TraceUtils.log(err);
}
//and continue
var userName = null;
if (str) {
var authCookie = JSON.parse(str);
//validate authentication cookie
if (authCookie.user)
userName = authCookie.user;
}
if (typeof model === 'undefined' || model === null) {
//no authentication provider is defined
context.user = { name: userName || 'anonymous', authenticationType:'Basic' };
return callback();
}
//search for user
if (userName) {
//set user identity
context.user = model.convert({ name: userName, authenticationType:'Basic' });
}
else {
//an auth cookie was found but user data or user model does not exist
//set anonymous identity
context.user = model.convert(AuthHandler.ANONYMOUS_IDENTITY);
}
return callback();
}
else {
//set anonymous identity
if (model)
context.user = model.convert(AuthHandler.ANONYMOUS_IDENTITY);
else
context.user = AuthHandler.ANONYMOUS_IDENTITY;
//no auth cookie was found on request
return callback();
}
}
catch (err) {
return callback(err);
}
};
/**
*
* @param {PreExecuteResultArgs} args
* @param {Function} callback
*/
AuthHandler.prototype.preExecuteResult = function (args, callback) {
try {
callback = callback || function() {};
var context = args.context, model = context.model('User');
if (typeof model === 'undefined' || model === null) {
return callback();
}
var authenticationType = context.user.authenticationType;
model.where('name').equal(context.user.name).expand('groups').silent().first(function(err, result) {
if (err) { return callback(err); }
if (result) {
//replace context.user with data object
context.user = model.convert(result);
context.user.authenticationType = authenticationType;
return callback();
}
else if (context.user.name!=='anonymous') {
model.where('name').equal('anonymous').expand('groups').silent().first(function(err, result) {
if (err) {
return callback(err);
}
if (result) {
context.user = model.convert(result);
context.user.authenticationType = authenticationType;
}
return callback();
});
}
else {
//do nothing
return callback();
}
});
}
catch (err) {
callback(err);
}
};
/**
* Creates a new instance of AuthHandler class
* @returns {AuthHandler}
*/
AuthHandler.createInstance = function() {
return new AuthHandler();
};
/**
* @abstract
* @class
* @constructor
* @augments HttpApplicationService
* @param {HttpApplication} app
*/
function AuthStrategy(app) {
AuthStrategy.super_.bind(this)(app);
if (this.constructor === AuthStrategy.prototype.constructor) {
throw new AbstractClassError();
}
}
LangUtils.inherits(AuthStrategy, HttpApplicationService);
// noinspection JSUnusedLocalSymbols
/**
* Sets the authentication cookie for the given context
* @param {HttpContext} thisContext
* @param {string} userName
* @param {*} options
* @abstract
*/
// eslint-disable-next-line no-unused-vars
AuthStrategy.prototype.setAuthCookie = function(thisContext, userName, options) {
throw new AbstractMethodError();
};
// noinspection JSUnusedLocalSymbols
/**
* Gets the authentication cookie of the given context
* @param {HttpContext} thisContext
* @returns {*}
* @abstract
*/
// eslint-disable-next-line no-unused-vars
AuthStrategy.prototype.getAuthCookie = function(thisContext) {
throw new AbstractMethodError();
};
// noinspection JSUnusedGlobalSymbols
/**
* Validates the specified credentials and authorizes the given context by setting the authorization cookie
* @param {HttpContext} thisContext - The current context
* @param userName - A string which represents the user name
* @param userPassword - A string which represents the user password
* @returns {Promise}
* @abstract
*/
// eslint-disable-next-line no-unused-vars
AuthStrategy.prototype.login = function(thisContext, userName, userPassword) {
throw new AbstractMethodError();
};
// noinspection JSUnusedGlobalSymbols
// noinspection JSUnusedLocalSymbols
/**
* Removes any authorization assigned to the given context
* @param {HttpContext} thisContext
* @returns {Promise}
* @abstract
*/
// eslint-disable-next-line no-unused-vars
AuthStrategy.prototype.logout = function(thisContext) {
throw new AbstractMethodError();
};
// noinspection JSUnusedGlobalSymbols
/**
* Gets the unattended execution account
* @returns {string}
* @abstract
*/
AuthStrategy.prototype.getUnattendedExecutionAccount = function() {
throw new AbstractMethodError();
};
/**
* Gets the options of this authentication strategy
* @abstract
* @returns {*}
*/
AuthStrategy.prototype.getOptions = function() {
throw new AbstractMethodError();
};
/**
* @class
* @constructor
* @augments AuthStrategy
* @param {HttpApplication} app
*/
function DefaultAuthStrategy(app) {
DefaultAuthStrategy.super_.bind(this)(app);
//get cookie name (from configuration)
this[optionsProperty] = {
"name":".MAUTH",
"slidingExpiration": false,
"expirationTimeout":420,
"unattendedExecutionAccount":RandomUtils.randomChars(16)
};
//get keys
var keys = _.keys(this[optionsProperty]);
//pick authSetting based on the given keys
var authSettings = _.pick(app.getConfiguration().settings.auth, keys);
//and assign properties to default
_.assign(this[optionsProperty], authSettings);
}
LangUtils.inherits(DefaultAuthStrategy, AuthStrategy);
/**
* Gets the options of this authentication strategy
* @abstract
* @returns {*}
*/
DefaultAuthStrategy.prototype.getOptions = function() {
return this[optionsProperty];
};
/**
* Sets the authentication cookie for the given context
* @param {HttpContext} thisContext - The current HTTP context
* @param {string} userName - The username to authorize
* @param {*=} options - Any other option we need to include in authorization cookie
*/
DefaultAuthStrategy.prototype.setAuthCookie = function(thisContext, userName, options) {
var defaultOptions = { user:userName, dateCreated:new Date()};
var value;
var expires;
if (_.isObject(options)) {
value = JSON.stringify(_.assign(options, defaultOptions));
if (_.isDate(options['expires'])) {
expires = options['expires'].toUTCString();
}
}
else {
value = JSON.stringify(defaultOptions);
}
//set default expiration as it has been defined in application configuration
if (_.isNil(expires) && _.isNumber(this.getOptions().expirationTimeout)) {
var expirationTimeout = LangUtils.parseInt(this.getOptions().expirationTimeout);
if (expirationTimeout>0) {
expires = moment(new Date()).add(expirationTimeout,'minutes').toDate().toUTCString();
}
}
var str = this[optionsProperty].name.concat('=', this.getApplication().getEncryptionStrategy().encrypt(value)) + ';path=/';
if (typeof expires === 'string') {
str +=';expires=' + expires;
}
thisContext.response.setHeader('Set-Cookie',str);
};
/**
* Validates the specified credentials and authorizes the given context by setting the authorization cookie
* @param thisContext - The current context
* @param userName - A string which represents the user name
* @param userPassword - A string which represents the user password
* @returns {Promise|*}
*/
DefaultAuthStrategy.prototype.login = function(thisContext, userName, userPassword) {
var self = this;
return Q.nfbind(function(context, userName, password, callback) {
try {
context.model('user').where('name').equal(userName).select('id','enabled').silent().first(function(err, result) {
if (err) {
return callback(new Error('Login failed due to server error. Please try again or contact your system administrator.'));
}
if (_.isNil(result)) {
return callback(new HttpUnauthorizedError('Unknown username. Please try again.'));
}
if (!result.enabled) {
return callback(new HttpForbiddenError('The account is disabled. Please contact your system administrator.'));
}
//user was found
const model = context.model('UserCredential');
if (typeof model === 'undefined' || model === null) {
TraceUtils.log('UserCredential model is missing.');
return callback(new Error('Login failed due to server error.'));
}
model.where('id').equal(result.id).prepare()
.and('userPassword').equal('{clear}'.concat(userPassword))
.or('userPassword').equal('{md5}'.concat(crypto.createHash('md5').update(userPassword).digest('hex')))
.or('userPassword').equal('{sha1}'.concat(crypto.createHash('sha1').update(userPassword).digest('hex')))
.silent().count().then(function(count) {
if (count===1) {
//set cookie
self.setAuthCookie(context, userName);
context.user = { name: userName, authenticationType:'Basic' };
return callback(null, true);
}
return callback(new HttpUnauthorizedError('Unknown username or bad password.'));
}).catch(function(err) {
TraceUtils.log(err);
return callback(new Error('Login failed due to server error. Please try again or contact your system administrator.'));
});
});
}
catch (err) {
TraceUtils.log(err);
return callback(new Error('Login failed due to internal server error.'));
}
})(thisContext, userName, userPassword);
};
/**
* Removes any authorization assigned to the given context
* @param thisContext
* @returns {Promise|*}
*/
DefaultAuthStrategy.prototype.logout = function(thisContext) {
const self = this;
return Q.nfbind(function(callback) {
//set auth cookie
self.setAuthCookie(thisContext,'anonymous');
return callback();
})();
};
// JSUnusedGlobalSymbols
/**
* Gets the authentication cookie of the given context
* @param {HttpContext} thisContext
* @returns {*}
*/
DefaultAuthStrategy.prototype.getAuthCookie = function(thisContext) {
var name = this.getOptions().name;
var cookie = thisContext.getCookie(name);
if (cookie) {
return this.getApplication().getEncryptionStrategy().decrypt(cookie);
}
};
/**
* Gets the unattended execution account
* @returns {string}
*/
DefaultAuthStrategy.prototype.getUnattendedExecutionAccount = function() {
return this[optionsProperty].unattendedExecutionAccount;
};
/**
* @abstract
* @class
* @constructor
* @augments HttpApplicationService
* @param {HttpApplication} app
*/
function EncryptionStrategy(app) {
EncryptionStrategy.super_.bind(this)(app);
if (this.constructor === EncryptionStrategy.prototype.constructor) {
throw new AbstractClassError();
}
}
LangUtils.inherits(EncryptionStrategy, HttpApplicationService);
/**
* Encrypts the given data
* @param {*} data
* @returns {*}
* */
// eslint-disable-next-line no-unused-vars
EncryptionStrategy.prototype.encrypt = function(data) {
throw new AbstractMethodError();
};
/**
* Decrypts the given data
* @param {string} data
* @returns {*}
* */
// eslint-disable-next-line no-unused-vars
EncryptionStrategy.prototype.decrypt = function(data) {
throw new AbstractMethodError();
};
var cryptoProperty = Symbol('crypto');
/**
* @class
* @constructor
* @augments HttpApplicationService
* @param {HttpApplication} app
*/
function DefaultEncryptionStrategy(app) {
DefaultEncryptionStrategy.super_.bind(this)(app);
this[cryptoProperty] = { };
_.assign(this[cryptoProperty], app.getConfiguration().settings.crypto);
}
LangUtils.inherits(DefaultEncryptionStrategy, EncryptionStrategy);
/**
* @returns {*}
*/
DefaultEncryptionStrategy.prototype.getOptions = function() {
return this[cryptoProperty];
};
/**
* Encrypts the given data
* @param {*} data
* @returns {*}
* */
DefaultEncryptionStrategy.prototype.encrypt = function(data) {
if (_.isNil(data)) {
return;
}
Args.check(this.getApplication().hasService(EncryptionStrategy),'Encryption strategy is missing');
const options = this.getOptions();
//validate settings
Args.check(!_.isNil(options.algorithm), 'Data encryption algorithm is missing. The operation cannot be completed');
Args.check(!_.isNil(options.key), 'Data encryption key is missing. The operation cannot be completed');
//encrypt
const cipher = crypto.createCipher(options.algorithm, options.key);
return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
};
/**
* Decrypts the given data
* @param {string} data
* @returns {*}
* */
DefaultEncryptionStrategy.prototype.decrypt = function(data) {
if (_.isNil(data))
return;
Args.check(this.getApplication().hasService(EncryptionStrategy),'Encryption strategy is missing');
//validate settings
const options = this.getOptions();
//validate settings
Args.check(!_.isNil(options.algorithm), 'Data encryption algorithm is missing. The operation cannot be completed');
Args.check(!_.isNil(options.key), 'Data encryption key is missing. The operation cannot be completed');
//decrypt
const decipher = crypto.createDecipher(options.algorithm, options.key);
return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
};
if (typeof exports !== 'undefined') {
module.exports.AuthHandler = AuthHandler;
/**
* @returns {AuthHandler}
*/
module.exports.createInstance = function () {
return AuthHandler.createInstance()
};
module.exports.AuthStrategy = AuthStrategy;
module.exports.DefaultAuthStrategy = DefaultAuthStrategy;
module.exports.EncryptionStrategy = EncryptionStrategy;
module.exports.DefaultEncryptionStrategy = DefaultEncryptionStrategy;
}