UNPKG

18 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9var TraceUtils = require('@themost/common/utils').TraceUtils;
10var RandomUtils = require('@themost/common/utils').RandomUtils;
11var AbstractClassError = require('@themost/common/errors').AbstractClassError;
12var AbstractMethodError = require('@themost/common/errors').AbstractMethodError;
13var HttpUnauthorizedError = require('@themost/common/errors').HttpUnauthorizedError;
14var HttpForbiddenError = require('@themost/common/errors').HttpForbiddenError;
15var LangUtils = require('@themost/common/utils').LangUtils;
16var Args = require('@themost/common/utils').Args;
17var HttpApplicationService = require('./../types').HttpApplicationService;
18var Symbol = require('symbol');
19var _ = require('lodash');
20var moment = require('moment');
21var crypto = require('crypto');
22var Q = require('q');
23
24var optionsProperty = Symbol('options');
25
26/**
27 * @class
28 * @constructor
29 * @implements AuthenticateRequestHandler
30 * @implements PreExecuteResultHandler
31 */
32function AuthHandler() {
33 //
34}
35/**
36 * @param {IncomingMessage|ClientRequest} request
37 * @returns {*}
38 */
39AuthHandler.parseCookies = function(request) {
40 var list = {},
41 rc = request.headers.cookie;
42 rc && rc.split(';').forEach(function( cookie ) {
43 var parts = cookie.split('=');
44 list[parts.shift().trim()] = unescape(parts.join('='));
45 });
46 return list;
47};
48
49AuthHandler.ANONYMOUS_IDENTITY = { name: 'anonymous', authenticationType:'None' };
50
51/**
52 * Authenticates an HTTP request and sets user or anonymous identity.
53 * @param {HttpContext} context
54 * @param {Function} callback
55 */
56AuthHandler.prototype.authenticateRequest = function (context, callback) {
57 try {
58 callback = callback || function() {};
59 var cookies = {}, model = context.model('User');
60 var config = context.getApplication().getConfiguration();
61 var settings = config.settings ? (config.settings.auth || { }) : { } ;
62 settings.name = settings.name || '.MAUTH';
63 if (context && context.request)
64 cookies = AuthHandler.parseCookies(context.request);
65 if (cookies[settings.name]) {
66 var str = null;
67 try {
68 str =context.getApplication().getEncryptionStrategy().decrypt(cookies[settings.name]);
69 }
70 catch (err) {
71 TraceUtils.log(err);
72 }
73 //and continue
74 var userName = null;
75 if (str) {
76 var authCookie = JSON.parse(str);
77 //validate authentication cookie
78 if (authCookie.user)
79 userName = authCookie.user;
80 }
81 if (typeof model === 'undefined' || model === null) {
82 //no authentication provider is defined
83 context.user = { name: userName || 'anonymous', authenticationType:'Basic' };
84 return callback();
85 }
86 //search for user
87 if (userName) {
88 //set user identity
89 context.user = model.convert({ name: userName, authenticationType:'Basic' });
90 }
91 else {
92 //an auth cookie was found but user data or user model does not exist
93 //set anonymous identity
94 context.user = model.convert(AuthHandler.ANONYMOUS_IDENTITY);
95 }
96 return callback();
97 }
98 else {
99 //set anonymous identity
100 if (model)
101 context.user = model.convert(AuthHandler.ANONYMOUS_IDENTITY);
102 else
103 context.user = AuthHandler.ANONYMOUS_IDENTITY;
104 //no auth cookie was found on request
105 return callback();
106 }
107 }
108 catch (err) {
109 return callback(err);
110 }
111};
112/**
113 *
114 * @param {PreExecuteResultArgs} args
115 * @param {Function} callback
116 */
117AuthHandler.prototype.preExecuteResult = function (args, callback) {
118 try {
119 callback = callback || function() {};
120 var context = args.context, model = context.model('User');
121 if (typeof model === 'undefined' || model === null) {
122 return callback();
123 }
124 var authenticationType = context.user.authenticationType;
125 model.where('name').equal(context.user.name).expand('groups').silent().first(function(err, result) {
126 if (err) { return callback(err); }
127 if (result) {
128 //replace context.user with data object
129 context.user = model.convert(result);
130 context.user.authenticationType = authenticationType;
131 return callback();
132 }
133 else if (context.user.name!=='anonymous') {
134 model.where('name').equal('anonymous').expand('groups').silent().first(function(err, result) {
135 if (err) {
136 return callback(err);
137 }
138 if (result) {
139 context.user = model.convert(result);
140 context.user.authenticationType = authenticationType;
141 }
142 return callback();
143 });
144 }
145 else {
146 //do nothing
147 return callback();
148 }
149 });
150 }
151 catch (err) {
152 callback(err);
153 }
154};
155
156/**
157 * Creates a new instance of AuthHandler class
158 * @returns {AuthHandler}
159 */
160AuthHandler.createInstance = function() {
161 return new AuthHandler();
162};
163
164/**
165 * @abstract
166 * @class
167 * @constructor
168 * @augments HttpApplicationService
169 * @param {HttpApplication} app
170 */
171function AuthStrategy(app) {
172 AuthStrategy.super_.bind(this)(app);
173 if (this.constructor === AuthStrategy.prototype.constructor) {
174 throw new AbstractClassError();
175 }
176}
177LangUtils.inherits(AuthStrategy, HttpApplicationService);
178
179// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
180/**
181 * Sets the authentication cookie for the given context
182 * @param {HttpContext} thisContext
183 * @param {string} userName
184 * @param {*} options
185 * @abstract
186 */
187// eslint-disable-next-line no-unused-vars
188AuthStrategy.prototype.setAuthCookie = function(thisContext, userName, options) {
189 throw new AbstractMethodError();
190};
191// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
192/**
193 * Gets the authentication cookie of the given context
194 * @param {HttpContext} thisContext
195 * @returns {*}
196 * @abstract
197 */
198// eslint-disable-next-line no-unused-vars
199AuthStrategy.prototype.getAuthCookie = function(thisContext) {
200 throw new AbstractMethodError();
201};
202
203// noinspection JSUnusedGlobalSymbols,JSUnusedLocalSymbols
204/**
205 * Validates the specified credentials and authorizes the given context by setting the authorization cookie
206 * @param {HttpContext} thisContext - The current context
207 * @param userName - A string which represents the user name
208 * @param userPassword - A string which represents the user password
209 * @returns {Promise}
210 * @abstract
211 */
212// eslint-disable-next-line no-unused-vars
213AuthStrategy.prototype.login = function(thisContext, userName, userPassword) {
214 throw new AbstractMethodError();
215};
216
217// noinspection JSUnusedGlobalSymbols
218// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
219/**
220 * Removes any authorization assigned to the given context
221 * @param {HttpContext} thisContext
222 * @returns {Promise}
223 * @abstract
224 */
225// eslint-disable-next-line no-unused-vars
226AuthStrategy.prototype.logout = function(thisContext) {
227 throw new AbstractMethodError();
228};
229// noinspection JSUnusedGlobalSymbols
230/**
231 * Gets the unattended execution account
232 * @returns {string}
233 * @abstract
234 */
235AuthStrategy.prototype.getUnattendedExecutionAccount = function() {
236 throw new AbstractMethodError();
237};
238// noinspection JSUnusedGlobalSymbols
239/**
240 * Gets the options of this authentication strategy
241 * @abstract
242 * @returns {*}
243 */
244AuthStrategy.prototype.getOptions = function() {
245 throw new AbstractMethodError();
246};
247
248/**
249 * @class
250 * @constructor
251 * @augments AuthStrategy
252 * @param {HttpApplication} app
253 */
254function DefaultAuthStrategy(app) {
255 DefaultAuthStrategy.super_.bind(this)(app);
256 //get cookie name (from configuration)
257 this[optionsProperty] = {
258 "name":".MAUTH",
259 "slidingExpiration": false,
260 "expirationTimeout":420,
261 "unattendedExecutionAccount":RandomUtils.randomChars(16)
262 };
263 //get keys
264 var keys = _.keys(this[optionsProperty]);
265 //pick authSetting based on the given keys
266 var authSettings = _.pick(app.getConfiguration().settings.auth, keys);
267 //and assign properties to default
268 _.assign(this[optionsProperty], authSettings);
269}
270LangUtils.inherits(DefaultAuthStrategy, AuthStrategy);
271
272/**
273 * Gets the options of this authentication strategy
274 * @abstract
275 * @returns {*}
276 */
277DefaultAuthStrategy.prototype.getOptions = function() {
278 return this[optionsProperty];
279};
280/**
281 * Sets the authentication cookie for the given context
282 * @param {HttpContext} thisContext - The current HTTP context
283 * @param {string} userName - The username to authorize
284 * @param {*=} options - Any other option we need to include in authorization cookie
285 */
286DefaultAuthStrategy.prototype.setAuthCookie = function(thisContext, userName, options) {
287 var defaultOptions = { user:userName, dateCreated:new Date()};
288 var value;
289 var expires;
290 if (_.isObject(options)) {
291 value = JSON.stringify(_.assign(options, defaultOptions));
292 if (_.isDate(options['expires'])) {
293 expires = options['expires'].toUTCString();
294 }
295 }
296 else {
297 value = JSON.stringify(defaultOptions);
298 }
299 //set default expiration as it has been defined in application configuration
300 if (_.isNil(expires) && _.isNumber(this.getOptions().expirationTimeout)) {
301 var expirationTimeout = LangUtils.parseInt(this.getOptions().expirationTimeout);
302 if (expirationTimeout>0) {
303 expires = moment(new Date()).add(expirationTimeout,'minutes').toDate().toUTCString();
304 }
305 }
306 var str = this[optionsProperty].name.concat('=', this.getApplication().getEncryptionStrategy().encrypt(value)) + ';path=/';
307 if (typeof expires === 'string') {
308 str +=';expires=' + expires;
309 }
310 thisContext.response.setHeader('Set-Cookie',str);
311};
312
313// noinspection JSUnusedGlobalSymbols
314/**
315 * Validates the specified credentials and authorizes the given context by setting the authorization cookie
316 * @param thisContext - The current context
317 * @param userName - A string which represents the user name
318 * @param userPassword - A string which represents the user password
319 * @returns {Promise|*}
320 */
321DefaultAuthStrategy.prototype.login = function(thisContext, userName, userPassword) {
322 var self = this;
323 return Q.nfbind(function(context, userName, password, callback) {
324 try {
325 context.model('user').where('name').equal(userName).select('id','enabled').silent().first(function(err, result) {
326 if (err) {
327 return callback(new Error('Login failed due to server error. Please try again or contact your system administrator.'));
328 }
329 if (_.isNil(result)) {
330 return callback(new HttpUnauthorizedError('Unknown username. Please try again.'));
331 }
332 if (!result.enabled) {
333 return callback(new HttpForbiddenError('The account is disabled. Please contact your system administrator.'));
334 }
335 //user was found
336 var model = context.model('UserCredential');
337 if (typeof model === 'undefined' || model === null) {
338 TraceUtils.log('UserCredential model is missing.');
339 return callback(new Error('Login failed due to server error.'));
340 }
341 model.where('id').equal(result.id).prepare()
342 .and('userPassword').equal('{clear}'.concat(userPassword))
343 .or('userPassword').equal('{md5}'.concat(crypto.createHash('md5').update(userPassword).digest('hex')))
344 .or('userPassword').equal('{sha1}'.concat(crypto.createHash('sha1').update(userPassword).digest('hex')))
345 .silent().count().then(function(count) {
346 if (count===1) {
347 //set cookie
348 self.setAuthCookie(context, userName);
349 context.user = { name: userName, authenticationType:'Basic' };
350 return callback(null, true);
351 }
352 return callback(new HttpUnauthorizedError('Unknown username or bad password.'));
353 }).catch(function(err) {
354 TraceUtils.log(err);
355 return callback(new Error('Login failed due to server error. Please try again or contact your system administrator.'));
356 });
357 });
358 }
359 catch (err) {
360 TraceUtils.log(err);
361 return callback(new Error('Login failed due to internal server error.'));
362 }
363
364 })(thisContext, userName, userPassword);
365
366};
367
368// noinspection JSUnusedGlobalSymbols
369/**
370 * Removes any authorization assigned to the given context
371 * @param thisContext
372 * @returns {Promise|*}
373 */
374DefaultAuthStrategy.prototype.logout = function(thisContext) {
375 var self = this;
376 return Q.nfbind(function(callback) {
377 //set auth cookie
378 self.setAuthCookie(thisContext,'anonymous');
379 return callback();
380 })();
381};
382// JSUnusedGlobalSymbols
383// noinspection JSUnusedGlobalSymbols
384/**
385 * Gets the authentication cookie of the given context
386 * @param {HttpContext} thisContext
387 * @returns {*}
388 */
389DefaultAuthStrategy.prototype.getAuthCookie = function(thisContext) {
390 var name = this.getOptions().name;
391 var cookie = thisContext.getCookie(name);
392 if (cookie) {
393 return this.getApplication().getEncryptionStrategy().decrypt(cookie);
394 }
395};
396
397// noinspection JSUnusedGlobalSymbols
398/**
399 * Gets the unattended execution account
400 * @returns {string}
401 */
402DefaultAuthStrategy.prototype.getUnattendedExecutionAccount = function() {
403 return this[optionsProperty].unattendedExecutionAccount;
404};
405
406/**
407 * @abstract
408 * @class
409 * @constructor
410 * @augments HttpApplicationService
411 * @param {HttpApplication} app
412 */
413function EncryptionStrategy(app) {
414 EncryptionStrategy.super_.bind(this)(app);
415 if (this.constructor === EncryptionStrategy.prototype.constructor) {
416 throw new AbstractClassError();
417 }
418}
419LangUtils.inherits(EncryptionStrategy, HttpApplicationService);
420
421// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
422/**
423 * Encrypts the given data
424 * @abstract
425 * @param {*} data
426 * @returns {*}
427 * */
428// eslint-disable-next-line no-unused-vars
429EncryptionStrategy.prototype.encrypt = function(data) {
430 throw new AbstractMethodError();
431};
432
433// noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols
434/**
435 * @abstract
436 * Decrypts the given data
437 * @param {string} data
438 * @returns {*}
439 * */
440// eslint-disable-next-line no-unused-vars
441EncryptionStrategy.prototype.decrypt = function(data) {
442 throw new AbstractMethodError();
443};
444
445var cryptoProperty = Symbol('crypto');
446
447/**
448 * @class
449 * @constructor
450 * @augments HttpApplicationService
451 * @param {HttpApplication} app
452 */
453function DefaultEncryptionStrategy(app) {
454 DefaultEncryptionStrategy.super_.bind(this)(app);
455 this[cryptoProperty] = { };
456 _.assign(this[cryptoProperty], app.getConfiguration().settings.crypto);
457}
458LangUtils.inherits(DefaultEncryptionStrategy, EncryptionStrategy);
459/**
460 * @returns {*}
461 */
462DefaultEncryptionStrategy.prototype.getOptions = function() {
463 return this[cryptoProperty];
464};
465// noinspection JSUnusedGlobalSymbols
466/**
467 * Encrypts the given data
468 * @param {*} data
469 * @returns {*}
470 * */
471DefaultEncryptionStrategy.prototype.encrypt = function(data) {
472 if (_.isNil(data)) {
473 return;
474 }
475 Args.check(this.getApplication().hasService(EncryptionStrategy),'Encryption strategy is missing');
476 var options = this.getOptions();
477 //validate settings
478 Args.check(!_.isNil(options.algorithm), 'Data encryption algorithm is missing. The operation cannot be completed');
479 Args.check(!_.isNil(options.key), 'Data encryption key is missing. The operation cannot be completed');
480 //encrypt
481 var cipher = crypto.createCipher(options.algorithm, options.key);
482 return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
483};
484
485// noinspection JSUnusedGlobalSymbols
486/**
487 * Decrypts the given data
488 * @param {string} data
489 * @returns {*}
490 * */
491DefaultEncryptionStrategy.prototype.decrypt = function(data) {
492 if (_.isNil(data))
493 return;
494 Args.check(this.getApplication().hasService(EncryptionStrategy),'Encryption strategy is missing');
495 //validate settings
496 var options = this.getOptions();
497 //validate settings
498 Args.check(!_.isNil(options.algorithm), 'Data encryption algorithm is missing. The operation cannot be completed');
499 Args.check(!_.isNil(options.key), 'Data encryption key is missing. The operation cannot be completed');
500 //decrypt
501 var decipher = crypto.createDecipher(options.algorithm, options.key);
502 return decipher.update(data, 'hex', 'utf8') + decipher.final('utf8');
503};
504
505
506if (typeof exports !== 'undefined') {
507 module.exports.AuthHandler = AuthHandler;
508 /**
509 * @returns {AuthHandler}
510 */
511 module.exports.createInstance = function () {
512 return AuthHandler.createInstance()
513 };
514 module.exports.AuthStrategy = AuthStrategy;
515 module.exports.DefaultAuthStrategy = DefaultAuthStrategy;
516 module.exports.EncryptionStrategy = EncryptionStrategy;
517 module.exports.DefaultEncryptionStrategy = DefaultEncryptionStrategy;
518}