UNPKG

orator-session

Version:

Session state and authentication manager for Orator Restful web API server.

718 lines (633 loc) 21.6 kB
/** * Generalized session state and auth manager for Orator/restify * * @class OratorSession * @constructor */ var OratorSession = function() { function createNew(pFable) { // If a valid fable object isn't passed in, return a constructor if ((typeof(pFable) !== 'object') || !('fable' in pFable)) return {new: createNew}; var _Settings = pFable.settings; var _Log = pFable.log; //Default settings value for session cookie name if (!_Settings.SessionCookieName) _Settings.SessionCookieName = 'UserSession'; if (!_Settings.SessionTempTokenTimeout) _Settings.SessionTempTokenTimeout = 60; if (!_Settings.SessionStrategy || //TODO: improve this settings fallback (_Settings.SessionStrategy !== 'Memcached' && _Settings.SessionStrategy !== 'InMemory')) _Settings.SessionStrategy = 'Memcached'; var libCookieParser = require('restify-cookies'); var libUUIDGenerator = require('fable-uuid').new(pFable.settings); var libAsync = require('async'); var libMoment = require('moment'); var libSessionStore = require(__dirname + '/strategies/' + _Settings.SessionStrategy).new(pFable); /** * Wire up routes for the OratorSession * * @method connectRoutes * @param {Object} pRestServer The Restify server object to add routes to */ var connectRoutes = function(pRestServer) { pRestServer.use(libCookieParser.parse); // This means the getSession is called on every request pRestServer.use(getSession); // This checks for a temp session token on every request pRestServer.use(getTempSession); //import session when ?SessionToken=temp_token_id // This logs each request after the session is set pRestServer.use(logSession); // Deauthenticate pRestServer.get('1.0/Deauthenticate', deAuthenticateUser); pRestServer.get('1.0/CheckSession', checkSession); //checkout a temp token which allows 3rd party connection to use this session pRestServer.get('1.0/CheckoutSessionToken', getSessionToken); //We could add routes here to support different auth-types // depending on configuration (WWW-Auth for example) //In case of an Orator proxy, these endpoints need to be omitted if (pFable.omitProxyRoute) { pFable.omitProxyRoute('1.0/Deauthenticate'); pFable.omitProxyRoute('1.0/CheckSession'); pFable.omitProxyRoute('1.0/CheckoutSessionToken'); } }; /** * Get SessionID */ var getSessionID = function(pRequest) { //SessionID first from Authorization header, then from session cookie, then fallback to session state object let tmpSessionID = null; if (pRequest.headers.hasOwnProperty('authorization') && pRequest.headers.authorization.indexOf('Bearer') == 0) { tmpSessionID = pRequest.headers.authorization.split(" ")[1]; } else { tmpSessionID = pRequest.cookies[_Settings.SessionCookieName]; } if (!tmpSessionID) { //this happens when new cookie is set but not received by client if (pRequest[_Settings.SessionCookieName]) tmpSessionID = pRequest[_Settings.SessionCookieName].SessionID; } return tmpSessionID; } /** * Get a Session (creating one if it doesn't exist) * * @method getSession */ var getSession = function getSession(pRequest, pResponse, fNext) { //TODO: maybe create a registry for this if (pRequest.url.indexOf('/ping.html') === 0 || pRequest.url.indexOf('/version') === 0) return fNext(); //no session check required if ((typeof(getSessionID(pRequest)) === 'undefined') || (getSessionID(pRequest) === '')) { return createSession(pRequest, pResponse, fNext); } else { //_Log.trace('Cookie reports session '+getSessionID(pRequest)); libSessionStore.get(getSessionID(pRequest), function(pError, pData) { if (pError) { _Log.trace('Session ID not found but cookie exists, creating a new session'+pError, {SessionID:getSessionID(pRequest)}); return createSession(pRequest, pResponse, fNext); } else { if (typeof(pData) === 'undefined') { return createSession(pRequest, pResponse, fNext); } else { //_Log.trace('Restoring session', {SessionID:getSessionID(pRequest)}); // Touch the session so we reset timeout. libSessionStore.touch(getSessionID(pRequest), _Settings.SessionTimeout, function (pError) { /* TODO: Log errors on the touch. */ }); pRequest[_Settings.SessionCookieName] = JSON.parse(pData) return fNext(); } } } ); } }; /** * Get a Temp Session * * @method getTempSession * @params Querystring: ?SessionToken=temp_token_id */ var getTempSession = function(pRequest, pResponse, fNext) { if (!pRequest.query.SessionToken) return fNext(); //validate SessionToken libSessionStore.get(pRequest.query.SessionToken, function(pError, pSessionIdentifierData) { if (!pSessionIdentifierData) { _Log.error('Failed to find temp session token '+pRequest.query.SessionToken); return fNext(); } //verify that parent session is still active libSessionStore.get(pSessionIdentifierData, function(pError, pData) { if (pError) { _Log.error('Failed to find session ' + pSessionIdentifierData + ' using temp token '+pRequest.query.SessionToken); return fNext(); } var tmpSession = {}; try { tmpSession = JSON.parse(pData); } catch (ex) { _Log.error('Invalid login request data: ' + ex); return fNext(); } if (!tmpSession.LoggedIn) { _Log.error('User session ' + pSessionIdentifierData + ' is no longer logged in!'); return fNext(); } _Log.trace('Session import using temp session token', { SessionID: pRequest[_Settings.SessionCookieName].SessionID, ParentSessionID: tmpSession.SessionID, TempSessionToken: pRequest.query.SessionToken }); //allow this user session to import attributes from parent session tmpSession.SessionID = pRequest[_Settings.SessionCookieName].SessionID; setSessionLoginStatus(pRequest, tmpSession); return fNext(); }); }); } /** * Get the currently logged in user */ var checkSession = function(pRequest, pResponse, fNext) { var tmpNext = (typeof(fNext) === 'function') ? fNext : function() {}; if (!pRequest[_Settings.SessionCookieName].LoggedIn) { pResponse.send({IDUser: 0, UserID:0, LoggedIn: false}); return tmpNext(); } var tmpIDUser = pRequest[_Settings.SessionCookieName].UserID; if (tmpIDUser < 1) { pResponse.send({IDUser: 0, UserID:0, LoggedIn: false}); return tmpNext(); } pResponse.send(pRequest[_Settings.SessionCookieName]); return tmpNext(); }; /** * Create a session in memcache * * @method createSession */ var createSession = function(pRequest, pResponse, fNext) { // Create a new session UUID... var tmpUUID = libUUIDGenerator.getUUID(); var tmpSessionID = 'SES'+tmpUUID; var tmpNewSessionData = formatEmptyUserPacket(tmpSessionID, tmpUUID); if (pRequest.SessionOverrideData) { _Log.info('SessionOverride data is specified.'); tmpSessionID = pRequest.SessionOverrideData.SessionID; tmpNewSessionData = pRequest.SessionOverrideData; } _Log.info('Creating a new session', {SessionID:tmpSessionID}); // This is the state stored in Memcached // We store this much to prevent roundtrips to the database each request var tmpNewSessionDataString = JSON.stringify(tmpNewSessionData); var tmpCookieDomain = getWildcardCookieDomain(pRequest); libSessionStore.get(tmpSessionID, function(pError, pData) { if (pError) { //_Log.trace('Error checking if session exists in memcache'+pError, {SessionID:tmpSessionID}); libSessionStore.set(tmpSessionID, tmpNewSessionDataString, _Settings.SessionTimeout, function(pError) { if (pError) _Log.trace('Error setting session: '+pError, {SessionID:tmpSessionID}); pRequest[_Settings.SessionCookieName] = tmpNewSessionData; pResponse.setCookie(_Settings.SessionCookieName,tmpNewSessionData.SessionID, {path: '/', maxAge: _Settings.SessionTimeout, httpOnly: true, domain: tmpCookieDomain }); return fNext(); } ); } else { if (typeof(pData === undefined)) { //_Log.trace('Session ID not found, creating', {SessionID:tmpSessionID}); libSessionStore.set(tmpSessionID, tmpNewSessionDataString, _Settings.SessionTimeout, function(pError) { if (pError) _Log.trace('Error setting session: '+pError, {SessionID:tmpSessionID}); pRequest[_Settings.SessionCookieName] = tmpNewSessionData; pResponse.setCookie(_Settings.SessionCookieName,tmpNewSessionData.SessionID, {path: '/', maxAge: _Settings.SessionTimeout, httpOnly: true, domain: tmpCookieDomain }); return fNext(); } ); } else { //_Log.trace('Session UUID collision.. this should NEVER happen', {SessionID:tmpSessionID, SessionData:pData}); libSessionStore.replace(tmpSessionID, tmpNewSessionDataString, 600, function(pError) { if (pError) _Log.trace('Error replacing session: '+pError, {SessionID:tmpSessionID}); pRequest[_Settings.SessionCookieName] = tmpNewSessionData; pResponse.setCookie(_Settings.SessionCookieName,tmpNewSessionData.SessionID, {path: '/', maxAge: _Settings.SessionTimeout, httpOnly: true, domain: tmpCookieDomain }); return fNext(); } ); } } } ); }; /** * Set the session login status * * @method setSessionLoginStatus * @param {Object} pRequest The request object to set a status on * @param {Boolean} pStatus The status of being logged in (true or false) * @param {String} pRole The role of the user */ var setSessionLoginStatus = function(pRequest, pUserPacket, fOptionalCallback) { if (!pUserPacket.SessionID) { //set the SessionID using cookie ID pUserPacket.SessionID = getSessionID(pRequest); } pRequest[_Settings.SessionCookieName] = pUserPacket; _Log.trace('Setting session status.', {SessionID:pRequest[_Settings.SessionCookieName].SessionID, Session: pRequest[_Settings.SessionCookieName]}); libSessionStore.replace(pRequest[_Settings.SessionCookieName].SessionID, JSON.stringify(pRequest[_Settings.SessionCookieName]), _Settings.SessionTimeout, function(pError) { if (pError) { _Log.trace('Error setting session status: '+pError, {SessionID:pRequest[_Settings.SessionCookieName].SessionID, Session: pRequest[_Settings.SessionCookieName]}); } if (fOptionalCallback) return fOptionalCallback(pError); } ); }; /** * Log session state on Request */ var logSession = function(pRequest, pResponse, fNext) { //TODO: maybe create a registry for this if (pRequest.url.indexOf('/ping.html') === 0 || pRequest.url.indexOf('/version') == 0) return fNext(); _Log.info('Request', { ClientIP:pRequest.connection.remoteAddress, RequestUUID:pRequest.RequestUUID, RequestURL:pRequest.url, SessionID:pRequest[_Settings.SessionCookieName].SessionID, CustomerID:pRequest[_Settings.SessionCookieName].CustomerID, UserID:pRequest[_Settings.SessionCookieName].UserID, }); // This duplicates the session data for the Meadow endpoints //TODO: change meadow to use _Settings.SessionCookieName pRequest.SessionData = pRequest[_Settings.SessionCookieName]; return fNext(); } /** * Check the session login status * * @method checkIfLoggedIn * @param {Object} pRequest The request object to check status on */ var checkIfLoggedIn = function(pRequest) { if ((typeof(getSessionID(pRequest)) === 'undefined') || (getSessionID(pRequest) === '')) { return false; } else { return (pRequest[_Settings.SessionCookieName].LoggedIn && pRequest[_Settings.SessionCookieName].UserID > 0); } }; /** * Log a user into the system using authenticator function * * @method authenticateUser * @param {Object} pRequest The request object which contains a Credentials object */ var authenticateUser = function(pRequest, fAuthenticator, fCallBack) { var remoteIP = pRequest.headers['x-forwarded-for'] || pRequest.connection.remoteAddress; _Log.trace('A user is attempting to login: ' + pRequest.Credentials.username, {RemoteIP: remoteIP, LoginID: pRequest.Credentials.username, Action: 'Authenticate-Attempt'}); // This will fail if the username or password are equal to false. Not exactly bad.... if (!pRequest.Credentials || !pRequest.Credentials.username || !pRequest.Credentials.password) { _Log.info('Authentication failure', {RemoteIP: remoteIP, LoginID: pRequest.Credentials.username, RequestID:pRequest.RequestUUID,Action:'Authenticate Validation',Success:false}); return fCallBack('Bad username or password!'); } fAuthenticator(pRequest.Credentials, function(err, loginUserPacketResult) { if (!loginUserPacketResult) loginUserPacketResult = formatEmptyUserPacket(getSessionID(pRequest)); var tmpStatus = (loginUserPacketResult.LoggedIn && loginUserPacketResult.UserID > 0) ? 'success' : 'failed'; _Log.trace('User login ' + tmpStatus, {LoginID: pRequest.Credentials.username, RequestID:pRequest.RequestUUID,Action:'Authenticate',Success: tmpStatus}); //set the SessionID using cookie ID loginUserPacketResult.SessionID = getSessionID(pRequest); //set memcache session to login result setSessionLoginStatus(pRequest, loginUserPacketResult); return fCallBack(err, loginUserPacketResult); }); }; /** * Default: Authenticate where credentials must match config * * @method defaultAuthenticator */ var defaultAuthenticator = function(pCredentials, fCallBack) { if (pCredentials.username === _Settings.DefaultUsername && pCredentials.password === _Settings.DefaultPassword) { return fCallBack(null, formatUserPacket(null, true, 'Administrator', 5, 1)); } else { return fCallBack('Invalid username or password!'); } }; /** * Log a user out from the system * * @method deAuthenticateUser */ var deAuthenticateUser = function(pRequest, pResponse, fNext) { _Log.info('Deauthentication success', {LoginID: pRequest[_Settings.SessionCookieName].LoginID, RequestID:pRequest.RequestUUID,Action:'Deauthenticate',Success:true}); var tmpUserPacket = formatEmptyUserPacket(pRequest[_Settings.SessionCookieName].SessionID); setSessionLoginStatus(pRequest, tmpUserPacket); pResponse.send({Success: true}) }; /** * Checkout a session token which can be used by 3rd-party connection to use this User session * * @method getSessionToken */ var getSessionToken = function(pRequest, pResponse, fCallback) { checkoutSessionToken(pRequest, function(err, token) { if (err) { pResponse.send({Error: err}); } else { pResponse.send({Token: token}); } return fCallback(); }); } /** * Checkout a session token which can be used by 3rd-party connection to use this User session * * @method checkoutSessionToken */ var checkoutSessionToken = function(pRequest, fCallback) { var tmpSession = pRequest[_Settings.SessionCookieName]; if (!tmpSession.LoggedIn) { return fCallback('User not logged in!'); } var tmpUUID = 'TempSessionToken-' + libUUIDGenerator.getUUID(); libSessionStore.set(tmpUUID, tmpSession.SessionID, _Settings.SessionTempTokenTimeout, function(pError) { if (pError) _Log.trace('Error checking out a session token!'+pError, {SessionID: tmpSession.SessionID}); else _Log.trace('Checked out a session token: ' + tmpUUID, {IDUser: tmpSession.UserID, SessionID: tmpSession.SessionID}); return fCallback(pError, tmpUUID); }); } /** * Lookup session from session store, but only retrieve related userID key * * @method getSessionUserID */ var getSessionUserID = function(pSessionID, fCallback) { libSessionStore.get(pSessionID, function(pError, pData) { var tmpUserID = 0; if (pData) { var tmpSessionData = JSON.parse(pData); if (tmpSessionData.UserID) tmpUserID = tmpSessionData.UserID; } return fCallback(pError, tmpUserID); }); } /** * Get the public-facing server domain name * * @method getServerHostDomain */ var getServerHostDomain = function(pRequest) { if (!pRequest || !pRequest.headers) { _Log.warn('getServerHostDomain -- request object missing headers!'); return false; } var tmpHostDomain = ''; if (pRequest.headers['origin']) //some reverse proxies will give us this header { //remove scheme tmpHostDomain = pRequest.headers['origin'].replace('http://', '').replace('https://', ''); } else { tmpHostDomain = pRequest.headers.host; } //remove port (just want domain) return tmpHostDomain.replace(/:.*/, ''); } /** * If the domain is >3 tiers, then return a domain with only the first 3 tiers (default for shared auth with microservices architecture) * e.g. myapp.mainapp.company.com -> mainapp.company.com * * @method getCookieDomain */ var getWildcardCookieDomain = function(pRequest) { var tmpHostDomain = getServerHostDomain(pRequest); //skip setting cookie domain for mobile apps if (pRequest.headers['user-agent'] && !!pRequest.headers['user-agent'].match(/iOS/)) { return null; } var domainParts = tmpHostDomain.split('.'); if (domainParts.length >= 3 && tmpHostDomain.indexOf('paviasystems')>0) { //For Pavia domains (e.g. *.headlight.paviasystems.com) return domainParts[domainParts.length-3] + '.' + domainParts[domainParts.length-2] + '.' + domainParts[domainParts.length-1]; } else if (domainParts.length >= 2 && tmpHostDomain.indexOf('paviasystems')<0) { //For other domains (e.g. *.idoteconstruction.com) return domainParts[domainParts.length-2] + '.' + domainParts[domainParts.length-1]; } else { //else don't use wildcards return null; } } //TODO: make this extensible var formatUserPacketFromRecord = function(pUserRecord) { return formatUserPacket( null, //set by authenticateUser true, //LoggedIn pUserRecord.UserRole, //amended property pUserRecord.IDRole, pUserRecord.IDUser, pUserRecord.IDCustomer, pUserRecord.Title, pUserRecord.NameFirst, pUserRecord.NameLast, pUserRecord.Email ); } //TODO: make this extensible var formatEmptyUserPacket = function(pSessionID) { return formatUserPacket( pSessionID, //SessionID false, //LoggedIn 'Unauthenticated', //UserRole -1, //UserRoleIndex 0, //UserID 0, //CustomerID '', //Title '', //NameFirst '', //NameLast '' //Email ); } //TODO: make this extensible var formatUserPacket = function(pSessionID, pStatus, pRole, pRoleIndex, pUserID, pCustomerID, pTitle, pNameFirst, pNameLast, pEmail) { return ( { Version: process.env.npm_package_version, SessionID: pSessionID, LoggedIn: pStatus, UserRole: pRole, UserRoleIndex: pRoleIndex, UserID: pUserID, CustomerID: pCustomerID, Title: pTitle, NameFirst: pNameFirst, NameLast: pNameLast, Email: pEmail }); } /** * Lookup session from session store. Retrieve related user records * * @method getSessionUserID */ var getActiveUserSessions = function(pSessionIDs, fCallback) { var tmpActiveUsers = []; libAsync.eachSeries(pSessionIDs, (pSessionID, fNext) => { libSessionStore.get(pSessionID, function(pError, pData) { if (pData) { var tmpSessionData = JSON.parse(pData); // This happen when user logout. SessionID still exists in the memcache but UserID = 0. if (tmpSessionData.UserID == 0) return fNext(); if (libMoment(tmpSessionData.LastLoginTime).format('YYYY-MM-DD HH:mm:ss') >= libMoment().subtract(15, 'minutes').format('YYYY-MM-DD HH:mm:ss')) tmpActiveUsers.push(tmpSessionData); } return fNext(); }); }, (pError) => { if (pError) return fCallback(pError); return fCallback(pError, tmpActiveUsers); }); } var sessionStore = function() { return libSessionStore; } var tmpOratorSession = ( { connectRoutes: connectRoutes, sessionStore: sessionStore, checkIfLoggedIn: checkIfLoggedIn, authenticateUser: authenticateUser, defaultAuthenticator: defaultAuthenticator, //remoteAuthenticator: remoteAuthenticator, createSession: createSession, checkSession: checkSession, deAuthenticateUser: deAuthenticateUser, checkoutSessionToken: checkoutSessionToken, getSessionUserID: getSessionUserID, formatEmptyUserPacket: formatEmptyUserPacket, formatUserPacketFromRecord: formatUserPacketFromRecord, formatUserPacket: formatUserPacket, setSessionLoginStatus: setSessionLoginStatus, getActiveUserSessions: getActiveUserSessions, new: createNew }); return tmpOratorSession; } return createNew(); }; module.exports = new OratorSession();