UNPKG

happn-3

Version:

pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb

261 lines (228 loc) 7.95 kB
module.exports = SecurityMiddleware; const commons = require('happn-commons'); const fs = commons.fs; const CONSTANTS = require('../../..').constants; const devNull = require('dev-null'); SecurityMiddleware.prototype.initialize = initialize; SecurityMiddleware.prototype.excluded = excluded; SecurityMiddleware.prototype.process = _process; SecurityMiddleware.prototype.__respondForbidden = __respondForbidden; SecurityMiddleware.prototype.__respondUnauthorized = __respondUnauthorized; SecurityMiddleware.prototype.__respond = __respond; function SecurityMiddleware() {} function initialize(config) { if (!config) config = {}; this.config = config; if (!this.config.exclusions) this.config.exclusions = []; } function excluded(req, next) { if (this.config.exclusions.length > 0) { var url = req.url.split('?')[0]; for (var patternIndex in this.config.exclusions) { var pattern = this.config.exclusions[patternIndex]; if (pattern === '/') { if (url === pattern) { next(); return true; } continue; // don't allow '/' exclusion into wildcardMatch (it always matches) } if (this.happn.services.utils.wildcardMatch(pattern, url)) { next(); return true; } } return false; } return false; } function _process(req, res, next) { if (!this.happn.config.secure) return next(); try { if (this.excluded(req, next)) return; if (req.url.substring(0, 1) !== '/') req.url = '/' + req.url; const parsedUrl = require('url').parse(req.url, true); const query = parsedUrl.query; let path = parsedUrl.pathname; const params = {}; if (path === '/auth/request-nonce') { params.publicKey = this.happn.services.utils.getFirstMatchingProperty( ['publicKey', 'public_key', 'public', 'key', 'public-key'], query ); try { const nonce = this.happn.services.security.createAuthenticationNonce(params); return this.__respond('nonce generated', nonce, null, res); } catch (e) { return next(e); } } if (path === '/auth/login') { params.username = this.happn.services.utils.getFirstMatchingProperty( ['user', 'username', 'u'], query ); params.password = this.happn.services.utils.getFirstMatchingProperty( ['password', 'pwd', 'pass', 'p'], query ); params.publicKey = this.happn.services.utils.getFirstMatchingProperty( ['publicKey', 'public_key', 'public', 'key', 'public-key', 'pk'], query ); params.token = this.happn.services.utils.getFirstMatchingProperty( ['token', 'happn_token', 't'], query ); params.digest = this.happn.services.utils.getFirstMatchingProperty(['digest'], query); //login type is stateless params.type = 0; const headers = this.happn.services.session.getClientUpgradeHeaders(req.headers); const address = { ip: req.connection.remoteAddress }; if (headers[CONSTANTS.CLIENT_HEADERS.X_FORWARDED_FOR] != null) address.ip = headers[CONSTANTS.CLIENT_HEADERS.X_FORWARDED_FOR].split(',')[0]; return this.happn.services.security.login( params, null, { data: { info: { _local: false } }, headers, address, encrypted: req.connection.encrypted, }, (e, session) => { if (e) { if ( e.message === 'Invalid credentials' || e.code === 401 || e.message === 'use of _ADMIN credentials over the network is disabled' ) { return this.__respond('login failed', null, e, res, 401); } return this.__respond('login failed', null, e, res, 500); } this.__respond('login successful', session.token, null, res); } ); } var session = this.happn.services.security.sessionFromRequest(req); if (!session) return this.__respondUnauthorized(req, res, 'invalid token format or null token'); var url = require('url'); path = '/@HTTP' + url.parse(req.url).pathname; session.type = 0; //stateless session this.happn.services.security.checkTokenUserId(session, (e, ok) => { if (e) return next(e); if (!ok) { return this.__respondUnauthorized( req, res, `token userid does not match userid for username: ${session.username}` ); } if (path === '/@HTTP/auth/logout') { if (!this.happn.services.security.config.allowLogoutOverHttp) { return this.__respondForbidden(req, res, 'logouts over http are not allowed'); } return this.happn.services.security.revokeToken(session.token, 'by user request', (e) => { if (e) { return next(e); } this.__respond('logout successful', session.token, null, res); }); } this.happn.services.security.authorize( session, path, req.method.toLowerCase(), (e, authorized, reason) => { if (e) { if (e.toString().indexOf('AccessDenied') === 0) return this.__respondForbidden(req, res, 'unauthorized access to path ' + path); return next(e); } if (!authorized) { if (CONSTANTS.UNAUTHORISED_REASONS_COLLECTION.indexOf(reason) > -1) return this.__respondUnauthorized( req, res, `authorization failed for ${session.username}: ${reason}` ); return this.__respondForbidden(req, res, 'unauthorized access to path ' + path); } req.happn_session = session; //used later if we are rechecking in security next(); } ); }); } catch (e) { next(e); } } function __respondForbidden(req, res, message) { var _this = this; if (!_this.config.forbiddenResponsePath) { res.writeHead(403, 'unauthorized access', { 'content-type': 'text/plain', }); res.write(message); req.pipe(devNull()).once('finish', () => { res.end(); }); return; } fs.readFile(_this.config.forbiddenResponsePath, function (err, html) { if (err) { res.writeHead(500); return res.end(_this.happn.services.utils.stringifyError(err)); } res.writeHead(403, 'unauthorized access', { 'Content-Type': 'text/html', }); res.end(html); }); } function __respondUnauthorized(req, res, message) { var _this = this; if (!_this.config.unauthorizedResponsePath) { res.writeHead(401, 'unauthorized access', { 'Content-Type': 'text/plain', 'WWW-Authenticate': 'happn-auth', }); res.write(message); req.pipe(devNull()).once('finish', () => { res.end(); }); return; } fs.readFile(_this.config.unauthorizedResponsePath, function (err, html) { if (err) { res.writeHead(500); return res.end(_this.happn.services.utils.stringifyError(err)); } res.writeHead(401, 'unauthorized access', { 'Content-Type': 'text/html', 'WWW-Authenticate': 'happn-auth', }); res.end(html); }); } function __respond(message, data, error, res, code) { var responseString = '{"message":"' + message + '", "data":{{DATA}}, "error":{{ERROR}}}'; var header = { 'Content-Type': 'application/json', }; if (error) { if (!code) code = 500; responseString = responseString.replace( '{{ERROR}}', this.happn.services.utils.stringifyError(error) ); } else { if (!code) code = 200; responseString = responseString.replace('{{ERROR}}', 'null'); } res.writeHead(code, header); if (data) responseString = responseString.replace('{{DATA}}', JSON.stringify(data)); else responseString = responseString.replace('{{DATA}}', 'null'); res.end(responseString); }