/lib/sessionManager.js

https://github.com/deitch/cansecurity · JavaScript · 305 lines · 242 code · 38 blank · 25 comment · 39 complexity · 03cd7662ca7275e7557eb7e849b57e4d MD5 · raw file

  1. /*global module, require, Buffer, console */
  2. const _ = require( 'lodash' ),
  3. crypto = require( 'crypto' ),
  4. tokenlib = require( './token' ),
  5. util = require('./util'),
  6. errors = require( './errors' ),
  7. sender = require('./sender'),
  8. constants = require( './constants' ).get(),
  9. now = util.now,
  10. warn = function (msg) {
  11. if (debug) {
  12. console.error(msg);
  13. }
  14. };
  15. const AUTHHEADER = constants.header.AUTH,
  16. AUTHMETHODHEADER = constants.header.AUTHMETHOD,
  17. AUTHSESSION = AUTHHEADER,
  18. CORSHEADER = constants.header.CORS,
  19. SESSIONEXPIRY = 15, // minutes
  20. RANDOM_STRING_LENGTH = 60,
  21. /*jslint regexp:true */
  22. MSGRE = /^error (.+)$/;
  23. /*jslint regexp:false */
  24. let validate, invalidTokenMessage, debug = false, sessionExpiry,
  25. encryptHeader = false;
  26. // set up warn on tokenlib
  27. tokenlib.setWarn(warn);
  28. ///////////////
  29. //// UTILS ////
  30. ///////////////
  31. const fnOrNull = ( f ) => ( f && typeof ( f ) === "function" ? f : null ),
  32. genRandomString = ( length ) => crypto.randomBytes( length ).toString( 'hex' )
  33. ;
  34. /////////////////////
  35. //// CREDENTIALS ////
  36. /////////////////////
  37. const requestHasBasicAuthCredentials = ( request ) => request.headers.authorization && request.headers.authorization.indexOf( "Basic " ) === 0,
  38. getBasicAuthCredentials = ( request ) => {
  39. const header = new Buffer( request.headers.authorization.split( ' ' )[ 1 ], 'base64' )
  40. .toString()
  41. .split( ":" );
  42. if ( header && header.length === 2 ) {
  43. return {
  44. user: header[ 0 ],
  45. password: header[ 1 ]
  46. };
  47. }
  48. return null;
  49. },
  50. getAuthCredentials = ( req ) => requestHasBasicAuthCredentials( req ) ? getBasicAuthCredentials( req ) : null,
  51. appendCORSHeader = ( response ) => {
  52. const existing = response.get( CORSHEADER ) || "";
  53. response.set( CORSHEADER, _.compact( existing.split( /,/ ) )
  54. .concat( [ AUTHHEADER ] )
  55. .join( "," ) );
  56. },
  57. getAuthTokenFromHeaders = ( req ) => {
  58. const header = req.headers.authorization,
  59. authToken = header && header.indexOf("Bearer ") === 0 ? header.split(' ')[1] : null;
  60. return authToken;
  61. };
  62. /////////////////
  63. //// SESSION ////
  64. /////////////////
  65. const setupSessionData = ( request, user, login, expiry ) => {
  66. if ( request.session ) {
  67. request.session[ AUTHSESSION ] = {
  68. user: user,
  69. login: login,
  70. expiry: expiry
  71. };
  72. request.session.touch();
  73. }
  74. },
  75. setupSessionHeaders = ( request, response, user, login, method, expiry ) => {
  76. const u = user || {}, userAsJson = JSON.stringify(u),
  77. header = [
  78. "success",
  79. tokenlib.generate( login, userAsJson, expiry ),
  80. login,
  81. expiry
  82. ];
  83. request[ AUTHHEADER ] = u;
  84. request[ AUTHMETHODHEADER ] = method;
  85. response.header( AUTHHEADER, header.join(" "));
  86. },
  87. removeSessionData = ( request ) => {
  88. if ( request.session ) {
  89. delete request.session[ AUTHSESSION ];
  90. }
  91. },
  92. clearHeaders = ( response ) => {
  93. response.removeHeader( AUTHHEADER );
  94. },
  95. endSession = ( request, response ) => {
  96. removeSessionData( request );
  97. clearHeaders( response );
  98. },
  99. cantStablishSession = ( req, res, next ) => {
  100. endSession( req, res );
  101. next();
  102. },
  103. prepareErrorHeaders = ( response, message ) => {
  104. response.header( AUTHHEADER, "error " + message );
  105. },
  106. endSessionWithErrorMessage = ( request, response, message ) => {
  107. removeSessionData( request );
  108. prepareErrorHeaders( response, message );
  109. },
  110. getSessionDataFromRequest = ( request ) => {
  111. const session = {};
  112. if ( request.session ) {
  113. session.auth = request.session[ AUTHSESSION ] || null;
  114. if ( session.auth ) {
  115. session.user = session.auth.user;
  116. }
  117. }
  118. return session;
  119. },
  120. startSession = ( config ) => {
  121. const req = config.req,
  122. res = config.res,
  123. expiry = now() + sessionExpiry;
  124. appendCORSHeader( res );
  125. setupSessionData( req, config.user, config.login, expiry );
  126. setupSessionHeaders( req, res, config.user, config.login, config.method, expiry );
  127. };
  128. ///////////////////
  129. //// CALLBACKS ////
  130. ///////////////////
  131. const validateCallback = function( success, user, message, pass ) {
  132. if ( success && user ) {
  133. warn(`Successfully validated ${user} via basic auth`);
  134. startSession( {
  135. req: this.req,
  136. res: this.res,
  137. user: user,
  138. login: this.creds.user,
  139. password: pass,
  140. method: constants.method.CREDENTIALS
  141. } );
  142. this.next();
  143. } else {
  144. warn(`Failed to validate user via basic auth`);
  145. endSessionWithErrorMessage( this.req, this.res, message );
  146. sender(this.res, 401, errors.unauthenticated( message ) );
  147. }
  148. };
  149. //////////////////////////
  150. //// SESSION CREATION ////
  151. //////////////////////////
  152. const tryStartSessionWithBasicAuthCredentials = ( req, res, next ) => {
  153. const creds = getAuthCredentials( req );
  154. warn(`Try Session with Basic Auth Creds for ${req.url}`);
  155. if ( creds ) {
  156. warn(`Basic Auth Creds found for ${creds.user}`);
  157. const context = {
  158. req: req,
  159. res: res,
  160. next: next,
  161. creds: creds
  162. };
  163. validate( creds.user, creds.password, validateCallback.bind( context ) );
  164. return true;
  165. }
  166. return false;
  167. },
  168. tryStartWithPreviousSessionData = ( req, res, next ) => {
  169. const session = getSessionDataFromRequest( req );
  170. let ret;
  171. warn(`Try Session with Previous express session data for ${req.url}`);
  172. if ( session.user ) {
  173. warn(`Session user found`);
  174. if ( session.auth.expiry > now() ) {
  175. warn(`Session still valid`);
  176. startSession( {
  177. req: req,
  178. res: res,
  179. user: session.user,
  180. login: session.auth.login,
  181. password: session.user.pass
  182. } );
  183. } else {
  184. warn(`Session expired`);
  185. endSession( req, res );
  186. }
  187. next();
  188. ret = true;
  189. } else {
  190. warn(`No previous session user found`);
  191. ret = false;
  192. }
  193. return ret;
  194. },
  195. tryStartSessionWithAuthToken = ( req, res, next ) => {
  196. const auth = getAuthTokenFromHeaders( req );
  197. let ret, token, login;
  198. warn("Try Session with Auth Token for "+req.url);
  199. if ( auth ) {
  200. warn(`Auth Token found`);
  201. // first validate the token
  202. token = tokenlib.validate( auth );
  203. // if succeeded, then we need to validate the user from the DB
  204. if ( token ) {
  205. login = token.sub;
  206. warn(`Successfully validated ${login} via auth token`);
  207. if ( validate ) {
  208. warn(`Trying to check user ...`);
  209. validate( token.sub, undefined, function (success, user ) {
  210. warn(`Tried validation, success? ${success}`);
  211. if (success) {
  212. startSession( {
  213. req: req,
  214. res: res,
  215. user: user,
  216. login: login,
  217. method: constants.method.TOKEN
  218. } );
  219. next();
  220. }
  221. } );
  222. }
  223. } else {
  224. warn(`Failed to validate ${login} via auth token`);
  225. endSessionWithErrorMessage( req, res, errors.invalidtoken( invalidTokenMessage ) );
  226. sender(res, 401, errors.invalidtoken( invalidTokenMessage ) );
  227. }
  228. ret = true;
  229. } else {
  230. warn(`No auth token found`);
  231. ret = false;
  232. }
  233. return ret;
  234. };
  235. /////////////////
  236. //// METHODS ////
  237. /////////////////
  238. const publicMethods = {
  239. // if there is a login session token, refresh its timeout on each request, and validate it
  240. validate: ( req, res, next ) => {
  241. "use strict";
  242. return tryStartSessionWithBasicAuthCredentials( req, res, next ) ||
  243. tryStartWithPreviousSessionData( req, res, next ) ||
  244. tryStartSessionWithAuthToken( req, res, next ) ||
  245. cantStablishSession( req, res, next );
  246. },
  247. clear: endSession,
  248. message: ( res ) => {
  249. const p = res.headers ? ( res.headers[ AUTHHEADER ] || res.headers[ AUTHHEADER.toLowerCase() ] || "" ) : "",
  250. match = MSGRE.exec( p ),
  251. msg = match && match.length > 1 && match[ 1 ].length > 0 ? match[ 1 ] : "";
  252. return ( msg );
  253. },
  254. getAuthMethod: ( req ) => ( req ? req[ AUTHMETHODHEADER ] : null ),
  255. getUser: ( req ) => req ? req[ AUTHHEADER ] : null
  256. };
  257. module.exports = {
  258. init: ( config ) => {
  259. validate = fnOrNull( config.validate );
  260. invalidTokenMessage = config.invalidTokenMessage || null;
  261. sessionExpiry = ( config.expiry || SESSIONEXPIRY ) * 60 * 1000;
  262. encryptHeader = ( config.encryptHeader || false );
  263. debug = config.debug || false;
  264. tokenlib.init( {key: config.sessionKey || genRandomString( RANDOM_STRING_LENGTH ), encrypt: encryptHeader, privateKey: config.privateKey, publicKey: config.publicKey } );
  265. return publicMethods;
  266. }
  267. };