diff --git a/.nvmrc b/.nvmrc index 53ebc989c..7e9b89991 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v7.4.0 \ No newline at end of file +v7.9.0 diff --git a/app.js b/app.js index a8a87079d..6ffd71419 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,7 @@ var path = require('path'); var favicon = require('serve-favicon'); var morganLogger = require('morgan'); var bodyParser = require('body-parser'); -var passport = require('passport'); -var session = require('express-session'); +// var passport = require('passport'); var flash = require('connect-flash'); var cookieParser = require('cookie-parser'); var fileUpload = require('express-fileupload'); @@ -12,13 +11,16 @@ var log = require('./libs/logger')(module); var recaptcha = require('express-recaptcha'); var compileSass = require('express-compile-sass'); var recaptchaConfig = require('config').get('recaptcha'); -var KnexSessionStore = require('connect-session-knex')(session); -var knex = require('./libs/db').knex; +const authExpress = require('./libs/auth-express'); + +// var session = require('express-session'); +// var KnexSessionStore = require('connect-session-knex')(session); +// var knex = require('./libs/db').knex; log.info('Spark is starting...'); // Creating Express application -var app = express(); +const app = express(); // FavIcon registration app.use(favicon(path.join(__dirname, '/public/favicon.ico'))); @@ -42,6 +44,8 @@ app.use(bodyParser.urlencoded({ app.use(cookieParser()); app.use(fileUpload()); +const auth = authExpress(app); + var root = process.cwd(); app.use(compileSass({ root: root + '/public', @@ -61,22 +65,22 @@ app.use(function(req, res, next) { next(); }); -// Passport setup -require('./libs/passport')(passport); - -// using session storage in DB - allows multiple server instances + cross session support between node js apps -var sessionStore = new KnexSessionStore({ - knex: knex -}); -app.use(session({ - secret: 'SparklePoniesAreFlyingOnEsplanade', //TODO check - should we put this on conifg / dotenv files? - resave: false, - saveUninitialized: false, - maxAge: 1000 * 60 * 30, - store: sessionStore -})); -app.use(passport.initialize()); -app.use(passport.session()); // persistent login sessions +// // Passport setup +// require('./libs/passport')(passport); +// +// // using session storage in DB - allows multiple server instances + cross session support between node js apps +// var sessionStore = new KnexSessionStore({ +// knex: knex +// }); +// app.use(session({ +// secret: 'SparklePoniesAreFlyingOnEsplanade', //TODO check - should we put this on conifg / dotenv files? +// resave: false, +// saveUninitialized: false, +// maxAge: 1000 * 60 * 30, +// store: sessionStore +// })); +// app.use(passport.initialize()); +// app.use(passport.session()); // persistent login sessions app.use(flash()); // use connect-flash for flash messages stored in session @@ -149,7 +153,7 @@ if (app.get('env') === 'development') { app.use('/dev', require('./routes/dev_routes')); require('./routes/fake_drupal')(app); } -require('./routes/main_routes.js')(app, passport); +require('./routes/main_routes.js')(app); app.use('/:lng?/admin', require('./routes/admin_routes')); @@ -163,24 +167,23 @@ var mail = require('./libs/mail'); mail.setup(app); // API -require('./routes/api_routes')(app, passport); +require('./routes/api_routes')(app, auth); // Camps / API // TODO this is not the right way to register routes -require('./routes/api_routes.js')(app, passport); -require('./routes/api_camps_routes')(app, passport); -require('./routes/camps_routes')(app, passport); +require('./routes/api_camps_routes')(app); +require('./routes/camps_routes')(app); require('./routes/api/v1/camps')(app); // CAMPS PUBLIC API -require('./routes/api_camps_routes')(app, passport); +require('./routes/api_camps_routes')(app); // Camps -require('./routes/camps_routes')(app, passport); +require('./routes/camps_routes')(app); //TODO this is not the right way to register routes var ticket_routes = require('./routes/ticket_routes'); app.use('/:lng/tickets/', ticket_routes); -require('./routes/volunteers_routes')(app, passport); +require('./routes/volunteers_routes')(app); // Recaptcha setup with siteId & secret recaptcha.init(recaptchaConfig.sitekey, recaptchaConfig.secretkey); @@ -190,14 +193,18 @@ log.info('Spark environment: NODE_ENV =', process.env.NODE_ENV, ', app.env =', a // ============== // Error handlers // ============== - -// Catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error('Not Found: ' + req.url); - err.status = 404; - next(err); +app.use(function (err, req, res, next) { + console.error(err.stack); + res.status(500).send('Something broke!') }); + // Catch 404 and forward to error handler + app.use(function(req, res, next) { + const err = new Error('Not Found: ' + req.url); + err.status = 404; + next(err); + }); + // Development error handler - will print stacktrace if (app.get('env') === 'development') { @@ -245,6 +252,10 @@ process.on('unhandledRejection', function(reason, p) { log.error("Possibly Unhandled Rejection at: Promise ", p, " reason: ", reason); }); +// process.on('uncaughtException', (reason, p) => { +// log.error("Possibly Unhandled Rejection at: Promise ", p, " reason: ", reason); +// }); + process.on('warning', function(warning) { log.warn(warning.name); // Print the warning name log.warn(warning.message); // Print the warning message diff --git a/libs/auth-express.js b/libs/auth-express.js new file mode 100644 index 000000000..32ee46409 --- /dev/null +++ b/libs/auth-express.js @@ -0,0 +1,33 @@ +const auth = require('./auth')(); +const unless = require('express-unless'); + +const devApi = path => path.startsWith('/dev'); +const loginUrl = path => path.startsWith('/jwt-login') || path.endsWith('/login') || path.endsWith('/signup') || path === '/'; +const staticContent = path => path.startsWith('/bower_components') || path.startsWith('/scripts'); + +const containLoginToken = req => (req.cookies && req.cookies[auth.SessionCookieName]) || (req.headers && req.headers['authorization']) || false; + +const shouldProtectApi = req => staticContent(req.path) || devApi(req.path) || (loginUrl(req.path) && !containLoginToken(req)); + +const sessionRenewalMiddleware = (req, res, next) => { + if (req.sparkSessionRenew) { + const sparkSession = req.sparkSession; + sparkSession.exp = Date.now() + auth.TokenTTL; + const token = auth.sign(sparkSession); + res.cookie(auth.SessionCookieName, token, {maxAge: 60 * 60 * 1000, httpOnly: true}); + delete req.sparkSessionRenew; + } + next(); +}; + +module.exports = app => { + app.use(auth.initialize()); + + const authenticate = auth.authenticate(); + authenticate.unless = unless; + + app.use(authenticate.unless(req => shouldProtectApi(req))); + app.use(sessionRenewalMiddleware); + + return auth; +}; diff --git a/libs/auth.js b/libs/auth.js new file mode 100644 index 000000000..198b459ae --- /dev/null +++ b/libs/auth.js @@ -0,0 +1,76 @@ +// auth.js +const passport = require("passport"); +const passportJWT = require("passport-jwt"); +const _ = require("lodash"); +const users = require("./users"); +const ExtractJwt = passportJWT.ExtractJwt; +const Strategy = passportJWT.Strategy; +const config = require('config'); +const jwt = require('jwt-simple'); +const apiTokensConfig = config.get('api_tokens'); + +const FiveMinutes = 5*60*1000; +const OneHour = 60*60*1000; + +const SessionCookieName = 'spark_session'; +const Algorithm = 'HS256'; +const TokenTTL = OneHour; +const TokenRenewalPeriod = FiveMinutes; + +const cookieExtractor = req => { + if (!_.isUndefined(req.cookies && req.cookies[SessionCookieName])) { + return req.cookies[SessionCookieName]; + } + return null; +}; + +const params = { + secretOrKey: apiTokensConfig.token, + jwtFromRequest: ExtractJwt.fromExtractors([cookieExtractor, ExtractJwt.fromAuthHeader()]), + passReqToCallback: true, + ignoreExpiration: true +}; + +const convertToSparkSession = user => { + return { + email: _.get(user, 'attributes.email', '') || '', + name: _.get(user, 'attributes.name', '') || '', + uid: _.get(user, 'attributes.user_id', -1), + exp: Date.now() + TokenTTL, + iat: Date.now() + }; +}; + +const sign = session => jwt.encode(session, apiTokensConfig.token, Algorithm); + +const isExpired = session => session.exp < Date.now(); +const isRenewable = session => (session.exp + TokenTTL + TokenRenewalPeriod) > Date.now(); + +module.exports = () => { + const strategy = new Strategy(params, (req, payload, done) => { + if (isExpired(payload)) { + if (isRenewable(payload)) { + req.sparkSessionRenew = true; + } else { + return done(null, false); + } + } + + req.sparkSession = payload; + return done(null, payload); + }); + passport.use(strategy); + return { + initialize: () => passport.initialize(), + authenticate: () => passport.authenticate("jwt", { session: false }), + login: (username, password) => { + return users.login(username, password) + .then(convertToSparkSession) + .then(sign) + }, + sign: sign, + SessionCookieName: SessionCookieName, + TokenTTL: TokenTTL, + TokenRenewalPeriod: TokenRenewalPeriod + }; +}; diff --git a/libs/passport.js b/libs/passport.js deleted file mode 100644 index faa49ac9e..000000000 --- a/libs/passport.js +++ /dev/null @@ -1,343 +0,0 @@ -var LocalStrategy = require('passport-local').Strategy; -var FacebookStrategy = require('passport-facebook').Strategy; -var i18next = require('i18next'); -var User = require('../models/user').User; -var config = require('config'); -var facebookConfig = config.get('facebook'); -var apiTokensConfig = config.get('api_tokens'); -var constants = require('../models/constants'); -var request = require('superagent'); -var _ = require('lodash'); -var jwt = require('jsonwebtoken'); -var passportJWT = require("passport-jwt"); -var ExtractJwt = passportJWT.ExtractJwt; -var JwtStrategy = passportJWT.Strategy; - -/*** - * tries to login based on drupal users table - * once user is successfully logged-in, an automatic sign-up flow is performed which creates a corresponding spark user - * @param email - * @param password - * @param done - */ -const drupal_login_request = (email, password) => - request - // .post('https://profile-test.midburn.org/api/user/login') - .post('https://profile.midburn.org/api/user/login') - .send({'username': email, 'password': password}) - .set('Accept', 'application/json') - .set('Content-Type', 'application/x-www-form-urlencoded') - .then(({ body }) => body, () => null); - -var login = function (email, password, done) { - if (!email || !password || email.length === 0 || password.length === 0) { - console.log('User', email, 'failed to authenticate.'); - done(false, null, i18next.t('invalid_user_password', { - email: email - })); - } - - // Loading user from DB. - User.forge({email: email}).fetch().then(function (user) { - if (user) { - // User found in DB, now checking everything: - if (!user.attributes.enabled) { - done(false, user, i18next.t('user_disabled')); - } - else if (!user.attributes.validated) { - done(false, user, i18next.t('user_not_validated')) - } - else if (!user.attributes.password || !user.validPassword(password)) { - done(false, user, i18next.t('invalid_user_password')); - } - else { - // Everything is OK, we're done. - done(true, user); - } - } - else { - done(false, null, i18next.t('invalid_user_password')); - } - }) -}; - -var drupal_login = function (email, password, done) { - login(email, password, function (isLoggedIn, user, error) { - drupal_login_request(email, password).then(function (drupal_user) { - if (drupal_user != null) { - // Drupal update information. - var drupal_user_id = drupal_user.user.uid; - var tickets = drupal_user.user.data.tickets.tickets; - var current_event_tickets_count = 0; - var tickets_array = []; - _.each(tickets, (ticket, ticket_id) => { - if (ticket.trid) { - var _ticket = { - trid: ticket.trid, - user_uid: ticket.user_uid, - bundle: ticket.bundle, - barcode: _.get(ticket, 'field_ticket_barcode.und.0.value', ''), - serial_id: _.get(ticket, 'field_ticket_serial_id.und.0.value', '') - }; - _ticket.is_mine = (_ticket.user_uid === drupal_user_id); - if (constants.events[constants.CURRENT_EVENT_ID].bundles.indexOf(_ticket.bundle) > -1) { - _ticket.event_id = constants.CURRENT_EVENT_ID; - if (_ticket.is_mine) { - current_event_tickets_count++; - } - } - tickets_array.push(_ticket); - } - }); - - var drupal_details = { - created_at: (new Date(parseInt(_.get(drupal_user, 'user.created', 0)) * 1000)).toISOString().substring(0, 19).replace('T', ' '), - updated_at: (new Date()).toISOString().substring(0, 19).replace('T', ' '), - first_name: _.get(drupal_user, 'user.field_profile_first.und.0.value', ''), - last_name: _.get(drupal_user, 'user.field_profile_last.und.0.value', ''), - cell_phone: _.get(drupal_user, 'user.field_profile_phone.und.0.value', ''), - address: _.get(drupal_user, 'user.field_profile_address.und.0.thoroughfare', '') + ' ' + _.get(drupal_user, 'user.field_profile_address.und.0.locality', '') + ' ' + _.get(drupal_user, 'user.field_profile_address.und.0.country', ''), - israeli_id: _.get(drupal_user, 'user.field_field_profile_document_id.und.0.value', ''), - date_of_birth: _.get(drupal_user, 'user.field_profile_birth_date.und.0.value', ''), - gender: _.get(drupal_user, 'user.field_sex.und.0.value', constants.USER_GENDERS_DEFAULT).toLowerCase(), - current_event_id_ticket_count: current_event_tickets_count, - validated: true - }; - - var details_json_data = { - address: { - first_name: _.get(drupal_user, 'user.field_profile_address.und.0.first_name', ''), - last_name: _.get(drupal_user, 'user.field_profile_address.und.0.last_name', ''), - street: _.get(drupal_user, 'user.field_profile_address.und.0.thoroughfare', ''), - city: _.get(drupal_user, 'user.field_profile_address.und.0.locality', ''), - country: _.get(drupal_user, 'user.field_profile_address.und.0.country', '') - }, - foreign_passport: _.get(drupal_user, 'user.field_profile_address.und.0.country', '') - }; - - var addinfo_json = {}; - if (user && typeof user.attributes.addinfo_json === 'string') { - addinfo_json = JSON.parse(user.attributes.addinfo_json); - } - addinfo_json.drupal_data = details_json_data; - addinfo_json.tickets = tickets_array; - drupal_details.addinfo_json = JSON.stringify(addinfo_json); - - if (user === null) { - signup(email, password, drupal_details, function (newUser, error) { - if (newUser) { - done(newUser); - } else { - done(false, error); - } - }) - } - else { - // we are now updating user data every login - // to fetch latest user information. very important is to know - // that once spark will be the main system, this all should be removed! - user.save(drupal_details).then((user) => { - done(user); - }); - - console.log('User', email, 'authenticated successfully in Drupal and synchronized to Spark.'); - } - } - else { - if (isLoggedIn) { - done(user); - } - else { - done(false, i18next.t('invalid_user_password')); - } - } - }); - }); -}; - -var signup = function (email, password, user, done) { - var userPromise = new User({ - email: email - }).fetch(); - userPromise.then(function (model) { - if (model) { - done(false, i18next.t('user_exists')) - } else { - var newUser = new User({ - email: email, - first_name: user.first_name, - last_name: user.last_name, - gender: user.gender, - validated: user.validated, - cell_phone: user.cell_phone - }); - if (password) { - newUser.generateHash(password) - } - if (!user.validated) { - newUser.generateValidation() - } - newUser.save().then(function (model) { - done(newUser) - }) - } - }) -}; - -var generateJwtToken = function (email) { - // from now on we'll identify the user by the email and the email - // is the only personalized value that goes into our token - var payload = {email: email}; - var token = jwt.sign(payload, apiTokensConfig.token); - return 'JWT ' + token; -}; - -// expose this function to our app using module.exports -module.exports = function (passport) { - // ========================================================================= - // passport session setup - // ========================================================================= - // required for persistent login sessions - // passport needs ability to serialize and deserialize users out of session - - // used to serialize the user for the session - passport.serializeUser(function (user, done) { - done(null, user.id) - }); - - // used to deserialize the user - passport.deserializeUser(function (id, done) { - new User({ - user_id: id - }).fetch().then(function (user) { - done(null, user) - }) - }); - - // ========================================================================= - // LOCAL SIGNUP - // ========================================================================= - passport.use('local-signup', new LocalStrategy({ - // by default, local strategy uses username and password, we will override with email - usernameField: 'email', - passwordField: 'password', - passReqToCallback: true // allows us to pass back the entire request to the callback - }, - function (req, email, password, done) { - signup(email, password, req.body, function (user, error) { - if (user) { - done(null, user, null) - } else { - done(null, false, req.flash('error', error)) - } - }) - })); - - // ========================================================================= - // LOCAL LOGIN - // ========================================================================= - passport.use('local-login', new LocalStrategy( - { - usernameField: 'email', - passwordField: 'password', - passReqToCallback: true - }, - function (req, email, password, done) { - drupal_login(email, password, function (user, error) { - if (user) { - done(null, user, null) - } else { - done(null, false, req.flash('error', error)) - } - }) - })); - - // ========================================================================= - // JWT authentication - // ========================================================================= - var jwtOptions = {}; - jwtOptions.jwtFromRequest = ExtractJwt.fromAuthHeader(); - jwtOptions.secretOrKey = apiTokensConfig.token; - - passport.use(new JwtStrategy(jwtOptions, function (jwt_payload, next) { - console.log('JWT payload received', jwt_payload); - var email = jwt_payload.email; - - new User({ - email: email - }).fetch().then(function (user) { - if (user) { - next(null, user); - } else { - next(null, false); - } - }); - })); - - // ========================================================================= - // Facebook login - // ========================================================================= - passport.use(new FacebookStrategy({ - clientID: facebookConfig.app_id, - clientSecret: facebookConfig.app_secret, - callbackURL: facebookConfig.callbackBase + '/auth/facebook/callback', - enableProof: true, - profileFields: ['id', 'email', 'first_name', 'last_name'] - }, - function (accessToken, refreshToken, profile, cb) { - if (profile.emails === undefined) { - // TODO: redirect user to http://lvh.me:3000/auth/facebook/reauth - console.log("User didn't agree to send us his email. "); - return cb(null, false) - } - - User.query({ - where: { - facebook_id: profile.id - }, - orWhere: { - email: profile.emails[0].value - } - }).fetch().then(function (model) { - if (model) { - // 1. Clear the user's password (the user will now only be - // able to login through FacebookStrategy) - // 2. Save updated token and details - model.save({ - password: '', - facebook_token: accessToken, - facebook_id: profile.id, - // I'm not quite sure about this. - // If a user changes his Facebook email, should we change - // it in our system? I think we should. Not convinced though. - email: profile.emails[0].values - }) - .then(function (_model) { - return cb(null, model, null) - }) - } else { - var newUser = new User({ - facebook_id: profile.id, - facebook_token: accessToken, - email: profile.emails[0].value, - first_name: profile.name.givenName, - last_name: profile.name.familyName, - gender: profile.gender, - validated: true - }); - - newUser.save().then(function (model) { - return cb(null, newUser, null) - }) - } - }) - } - )) -}; - -module.exports.sign_up = function (email, password, user, done) { - signup(email, password, user, done) -}; - -module.exports.login = login; -module.exports.generateJwtToken = generateJwtToken; diff --git a/libs/security-token.js b/libs/security-token.js deleted file mode 100644 index d9e1580d8..000000000 --- a/libs/security-token.js +++ /dev/null @@ -1,23 +0,0 @@ -const jwt = require('jsonwebtoken'); -const assert = require('assert'); - -const Algo = 'RS256'; - -const encrypt = (data, opts) => { - assert(opts, 'options.privateKey is mandatory'); - assert(opts.privateKey, 'options.privateKey is mandatory'); - - return jwt.sign(data, opts.privateKey, {algorithm: Algo}); -}; - -const decrypt = (data, opts) => { - assert(opts, 'options.publicKey is mandatory'); - assert(opts.publicKey, 'options.publicKey is mandatory'); - - return jwt.verify(data, opts.publicKey, { - algorithms: [Algo], - ignoreExpiration: opts.ignoreExpiration || false - }); -}; - -module.exports = {encrypt, decrypt}; diff --git a/libs/users.js b/libs/users.js new file mode 100644 index 000000000..1674f8d34 --- /dev/null +++ b/libs/users.js @@ -0,0 +1,197 @@ +const i18next = require('i18next'); +const User = require('../models/user').User; +const constants = require('../models/constants'); +const request = require('superagent'); +const _ = require('lodash'); + +/*** + * tries to login based on drupal users table + * once user is successfully logged-in, an automatic sign-up flow is performed which creates a corresponding spark user + * @param email + * @param password + * @param done + */ +const drupalLoginRequest = (email, password) => + // .post('https://profile-test.midburn.org/api/user/login') + request + .post('https://profile.midburn.org/api/user/login') + .send({'username': email, 'password': password}) + .set('Accept', 'application/json') + .set('Content-Type', 'application/x-www-form-urlencoded') + .then(({ body }) => body, () => null); + +const login = function (email, password, done) { + if (!email || !password || email.length === 0 || password.length === 0) { + console.log('User', email, 'failed to authenticate.'); + done(false, null, i18next.t('invalid_user_password', { + email: email + })); + } + + // Loading user from DB. + User.forge({email: email}).fetch().then(function (user) { + if (user) { + // User found in DB, now checking everything: + if (!user.attributes.enabled) { + done(false, user, i18next.t('user_disabled')); + } + else if (!user.attributes.validated) { + done(false, user, i18next.t('user_not_validated')) + } + else if (!user.attributes.password || !user.validPassword(password)) { + done(false, user, i18next.t('invalid_user_password')); + } + else { + // Everything is OK, we're done. + done(true, user); + } + } + else { + done(false, null, i18next.t('invalid_user_password')); + } + }) +}; + +const drupalLogin = function (email, password, done) { + login(email, password, function (isLoggedIn, user, error) { + drupalLoginRequest(email, password).then(function (drupal_user) { + if (drupal_user != null) { + // Drupal update information. + const drupal_user_id = drupal_user.user.uid; + const tickets = drupal_user.user.data.tickets.tickets; + let current_event_tickets_count = 0; + const tickets_array = []; + _.each(tickets, (ticket, ticket_id) => { + if (ticket.trid) { + const _ticket = { + trid: ticket.trid, + user_uid: ticket.user_uid, + bundle: ticket.bundle, + barcode: _.get(ticket, 'field_ticket_barcode.und.0.value', ''), + serial_id: _.get(ticket, 'field_ticket_serial_id.und.0.value', '') + }; + _ticket.is_mine = (_ticket.user_uid === drupal_user_id); + if (constants.events[constants.CURRENT_EVENT_ID].bundles.indexOf(_ticket.bundle) > -1) { + _ticket.event_id = constants.CURRENT_EVENT_ID; + if (_ticket.is_mine) { + current_event_tickets_count++; + } + } + tickets_array.push(_ticket); + } + }); + + const drupal_details = { + created_at: (new Date(parseInt(_.get(drupal_user, 'user.created', 0)) * 1000)).toISOString().substring(0, 19).replace('T', ' '), + updated_at: (new Date()).toISOString().substring(0, 19).replace('T', ' '), + first_name: _.get(drupal_user, 'user.field_profile_first.und.0.value', ''), + last_name: _.get(drupal_user, 'user.field_profile_last.und.0.value', ''), + cell_phone: _.get(drupal_user, 'user.field_profile_phone.und.0.value', ''), + address: _.get(drupal_user, 'user.field_profile_address.und.0.thoroughfare', '') + ' ' + _.get(drupal_user, 'user.field_profile_address.und.0.locality', '') + ' ' + _.get(drupal_user, 'user.field_profile_address.und.0.country', ''), + israeli_id: _.get(drupal_user, 'user.field_field_profile_document_id.und.0.value', ''), + date_of_birth: _.get(drupal_user, 'user.field_profile_birth_date.und.0.value', ''), + gender: _.get(drupal_user, 'user.field_sex.und.0.value', constants.USER_GENDERS_DEFAULT).toLowerCase(), + current_event_id_ticket_count: current_event_tickets_count, + validated: true + }; + + const details_json_data = { + address: { + first_name: _.get(drupal_user, 'user.field_profile_address.und.0.first_name', ''), + last_name: _.get(drupal_user, 'user.field_profile_address.und.0.last_name', ''), + street: _.get(drupal_user, 'user.field_profile_address.und.0.thoroughfare', ''), + city: _.get(drupal_user, 'user.field_profile_address.und.0.locality', ''), + country: _.get(drupal_user, 'user.field_profile_address.und.0.country', '') + }, + foreign_passport: _.get(drupal_user, 'user.field_profile_address.und.0.country', '') + }; + + let addinfo_json = {}; + if (user && typeof user.attributes.addinfo_json === 'string') { + addinfo_json = JSON.parse(user.attributes.addinfo_json); + } + addinfo_json.drupal_data = details_json_data; + addinfo_json.tickets = tickets_array; + drupal_details.addinfo_json = JSON.stringify(addinfo_json); + + if (user === null) { + signup(email, password, drupal_details, function (newUser, error) { + if (newUser) { + done(newUser); + } else { + done(false, error); + } + }) + } + else { + // we are now updating user data every login + // to fetch latest user information. very important is to know + // that once spark will be the main system, this all should be removed! + user.save(drupal_details).then((user) => { + done(user); + }); + + console.log('User', email, 'authenticated successfully in Drupal and synchronized to Spark.'); + } + } + else { + if (isLoggedIn) { + done(user); + } + else { + done(false, i18next.t('invalid_user_password')); + } + } + }); + }); +}; + +const signup = function (email, password, user, done) { + const userPromise = new User({ + email: email + }).fetch(); + userPromise.then(function (model) { + if (model) { + done(false, i18next.t('user_exists')) + } else { + const newUser = new User({ + email: email, + first_name: user.first_name, + last_name: user.last_name, + gender: user.gender, + validated: user.validated, + cell_phone: user.cell_phone + }); + if (password) { + newUser.generateHash(password) + } + if (!user.validated) { + newUser.generateValidation() + } + newUser.save().then(function (model) { + done(newUser) + }) + } + }) +}; + +const loginLocal = (email, password) => { + return new Promise((resolve, reject) => + drupalLogin(email, password, (user, error) => { + if (user) { + resolve(user); + } else { + reject(error); + }})); +}; + +module.exports = { + signup: signup, + + sign_up: function (email, password, user, done) { + signup(email, password, user, done) + }, + + login: loginLocal +}; + diff --git a/package.json b/package.json index 755885fd4..4d3c4d6bc 100644 --- a/package.json +++ b/package.json @@ -1,110 +1,114 @@ { - "name": "spark", - "//version": [ - "the version number is available to the scripts in the npm_package_version environment variable", - "this is part of the deployment proess, so it is important to update the version number", - "version name corresponds to the github release name / tag name - https://github.com/Midburn/Spark/releases" - ], - "version": "2.3.2", - "private": true, - "scripts": { - "postinstall": "bower install", - "start": "node server.js", - "pretest": "npm run lint", - "test": "npm run testcore", - "testcore": "npm run create_test_db && cross-env SPARK_DB_DBNAME=spark_test mocha \"tests/**/*test.js\" ", - "//devops": "Generic deployment / devops commands, used from .travis.yml, see /docs/development/releases-and-deployment.md", - "//log": "send a generic log notification to slack", - "log": "curl -X POST -g $npm_config_webhook --data-urlencode 'payload={\"channel\": \"'\"#${npm_config_channel:-sparksystem-log}\"'\", \"username\": \"'\"${npm_config_username:-bot}\"'\", \"text\": \"'\"${npm_config_text}\"'\", \"icon_emoji\": \"'\"${npm_config_emoji:-ghost}\"'\"}'", - "//build": "builds the deployment package", - "build": "touch \"${npm_config_file}\" && tar --exclude=.github* --exclude=.idea* --exclude=.git* --exclude=*.sqlite3 --exclude=opsworks.js --exclude=*.tar.gz --exclude=vagrant --exclude=docs --exclude=.travis* --exclude=*.iml --exclude=.env --exclude=*.log -czf \"${npm_config_file}\" . && echo \"created build package: ${npm_config_file}\"", - "//upload": "uploads the deployment package to slack and returns the package private url", - "upload": "curl -F \"file=@${npm_config_file}\" -F \"filename=${npm_config_file}\" -F \"token=${npm_config_token}\" \"${npm_config_url:-https://slack.com/api/files.upload}\" | jq -r .file.url_private > \"${npm_config_file}.slack_url\"", - "//download": "download a deployment package from private slack url", - "download": "curl -H \"Authorization: Bearer ${npm_config_token}\" -g \"${npm_config_url}\" -o \"${npm_config_file}\"", - "//deploy": "prepare the directory of an extracted deployment package", - "deploy": "echo SPARK_DEPLOYMENT_TIME=\\\"$(date \"+%F %T\")\\\" >> .env", - "//lint": "runs static analysis using eslint", - "lint": "./node_modules/.bin/eslint .", - "//createdb": "bootstraps MySql with a clean spark database & user", - "createdb": "mysql -u root < migrations/create_db.sql", - "create_test_db": "cross-env SPARK_DB_DBNAME=spark_test mysql -u root --protocol=tcp < migrations/create_test_db.sql && cross-env SPARK_DB_DBNAME=spark_test knex migrate:latest" - }, - "dependencies": { - "async": "^2.1.4", - "babel-core": "^6.21.0", - "babel-loader": "^6.2.10", - "babel-preset-react": "^6.16.0", - "babel-register": "^6.18.0", - "bcrypt-nodejs": "0.0.3", - "body-parser": "~1.15.1", - "bookshelf": "^0.10.0", - "bootstrap": "^3.3.7", - "bootstrap-validator": "^0.11.5", - "bower": "^1.8.0", - "config": "^1.21.0", - "connect-flash": "^0.1.1", - "connect-roles": "^3.1.2", - "connect-session-knex": "^1.3.1", - "cookie-parser": "~1.4.3", - "csurf": "^1.9.0", - "csv": "^1.1.1", - "dateformat": "1.0.12", - "debug": "~2.2.0", - "dotenv": "^4.0.0", - "drupal-hash": "^1.0.3", - "express": "~4.13.4", - "express-breadcrumbs": "0.0.2", - "express-compile-sass": "3.0.4", - "express-fileupload": "0.0.5", - "express-mailer": "^0.3.1", - "express-recaptcha": "^2.1.0", - "express-session": "^1.14.0", - "i18next": "^3.4.1", - "i18next-express-middleware": "^1.0.1", - "i18next-localstorage-cache": "^0.3.0", - "i18next-node-fs-backend": "^0.1.2", - "jade": "^1.11.0", - "jsonwebtoken": "^7.3.0", - "knex": "^0.12.9", - "lodash": "^4.17.4", - "module-id": "2.0.4", - "morgan": "~1.7.0", - "mysql": "^2.11.1", - "node-sass": "^3.13.0", - "passport": "^0.3.2", - "passport-facebook": "2.1.1", - "passport-jwt": "^2.2.1", - "passport-local": "^1.0.0", - "passport-remember-me": "^0.0.1", - "rand-token": "^0.2.1", - "react": "^15.4.1", - "react-dom": "^15.4.1", - "request": "^2.74.0", - "requirejs": "^2.3.2", - "serve-favicon": "~2.3.0", - "sprintf-js": "1.0.3", - "sqlite3": "^3.1.8", - "superagent": "^3.5.0", - "webpack": "^1.14.0", - "winston": "2.3.0" - }, - "devDependencies": { - "babel-cli": "^6.18.0", - "babel-core": "^6.21.0", - "babel-eslint": "^7.1.1", - "babel-loader": "^6.2.10", - "babel-preset-react": "^6.16.0", - "chai": "*", - "cross-env": "3.2.4", - "eslint": "^3.15.0", - "eslint-config-standard": "^6.2.1", - "eslint-plugin-promise": "^3.5.0", - "eslint-plugin-standard": "^2.1.1", - "mocha": "^3.2.0", - "nock": "^9.0.13", - "node-rsa": "^0.4.2", - "rimraf": "2.6.1", - "supertest": "^2.0.1" - } -} \ No newline at end of file + "name": "spark", + "//version": [ + "the version number is available to the scripts in the npm_package_version environment variable", + "this is part of the deployment proess, so it is important to update the version number", + "version name corresponds to the github release name / tag name - https://github.com/Midburn/Spark/releases" + ], + "version": "2.3.2", + "private": true, + "scripts": { + "postinstall": "bower install", + "start": "node server.js", + "pretest": "npm run lint", + "test": "npm run testcore", + "testcore": "npm run create_test_db && cross-env SPARK_DB_DBNAME=spark_test mocha \"tests/**/*test.js\" ", + "//devops": "Generic deployment / devops commands, used from .travis.yml, see /docs/development/releases-and-deployment.md", + "//log": "send a generic log notification to slack", + "log": "curl -X POST -g $npm_config_webhook --data-urlencode 'payload={\"channel\": \"'\"#${npm_config_channel:-sparksystem-log}\"'\", \"username\": \"'\"${npm_config_username:-bot}\"'\", \"text\": \"'\"${npm_config_text}\"'\", \"icon_emoji\": \"'\"${npm_config_emoji:-ghost}\"'\"}'", + "//build": "builds the deployment package", + "build": "touch \"${npm_config_file}\" && tar --exclude=.github* --exclude=.idea* --exclude=.git* --exclude=*.sqlite3 --exclude=opsworks.js --exclude=*.tar.gz --exclude=vagrant --exclude=docs --exclude=.travis* --exclude=*.iml --exclude=.env --exclude=*.log -czf \"${npm_config_file}\" . && echo \"created build package: ${npm_config_file}\"", + "//upload": "uploads the deployment package to slack and returns the package private url", + "upload": "curl -F \"file=@${npm_config_file}\" -F \"filename=${npm_config_file}\" -F \"token=${npm_config_token}\" \"${npm_config_url:-https://slack.com/api/files.upload}\" | jq -r .file.url_private > \"${npm_config_file}.slack_url\"", + "//download": "download a deployment package from private slack url", + "download": "curl -H \"Authorization: Bearer ${npm_config_token}\" -g \"${npm_config_url}\" -o \"${npm_config_file}\"", + "//deploy": "prepare the directory of an extracted deployment package", + "deploy": "echo SPARK_DEPLOYMENT_TIME=\\\"$(date \"+%F %T\")\\\" >> .env", + "//lint": "runs static analysis using eslint", + "lint": "./node_modules/.bin/eslint .", + "//createdb": "bootstraps MySql with a clean spark database & user", + "createdb": "mysql -u root < migrations/create_db.sql", + "create_test_db": "cross-env SPARK_DB_DBNAME=spark_test mysql -u root --protocol=tcp < migrations/create_test_db.sql && cross-env SPARK_DB_DBNAME=spark_test knex migrate:latest" + }, + "dependencies": { + "async": "^2.1.4", + "babel-core": "^6.21.0", + "babel-loader": "^6.2.10", + "babel-preset-react": "^6.16.0", + "babel-register": "^6.18.0", + "bcrypt-nodejs": "0.0.3", + "body-parser": "~1.15.1", + "bookshelf": "^0.10.0", + "bootstrap": "^3.3.7", + "bootstrap-validator": "^0.11.5", + "bower": "^1.8.0", + "config": "^1.21.0", + "connect-flash": "^0.1.1", + "connect-roles": "^3.1.2", + "connect-session-knex": "^1.3.1", + "cookie-parser": "~1.4.3", + "csurf": "^1.9.0", + "csv": "^1.1.1", + "dateformat": "1.0.12", + "debug": "~2.2.0", + "dotenv": "^4.0.0", + "drupal-hash": "^1.0.3", + "express": "~4.13.4", + "express-breadcrumbs": "0.0.2", + "express-compile-sass": "3.0.4", + "express-fileupload": "0.0.5", + "express-mailer": "^0.3.1", + "express-recaptcha": "^2.1.0", + "express-session": "^1.14.0", + "express-unless": "^0.3.0", + "i18next": "^3.4.1", + "i18next-express-middleware": "^1.0.1", + "i18next-localstorage-cache": "^0.3.0", + "i18next-node-fs-backend": "^0.1.2", + "jade": "^1.11.0", + "jsonwebtoken": "^7.3.0", + "jwt-simple": "^0.5.1", + "knex": "^0.12.9", + "lodash": "^4.17.4", + "module-id": "2.0.4", + "morgan": "~1.7.0", + "mysql": "^2.11.1", + "node-sass": "^3.13.0", + "passport": "^0.3.2", + "passport-facebook": "2.1.1", + "passport-jwt": "^2.2.1", + "passport-local": "^1.0.0", + "passport-remember-me": "^0.0.1", + "rand-token": "^0.2.1", + "react": "^15.4.1", + "react-dom": "^15.4.1", + "request": "^2.74.0", + "requirejs": "^2.3.2", + "serve-favicon": "~2.3.0", + "sprintf-js": "1.0.3", + "sqlite3": "^3.1.8", + "superagent": "^3.5.0", + "webpack": "^1.14.0", + "winston": "2.3.0" + }, + "devDependencies": { + "babel-cli": "^6.18.0", + "babel-core": "^6.21.0", + "babel-eslint": "^7.1.1", + "babel-loader": "^6.2.10", + "babel-preset-react": "^6.16.0", + "chai": "*", + "chance": "^1.0.8", + "cross-env": "3.2.4", + "eslint": "^3.15.0", + "eslint-config-standard": "^6.2.1", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^2.1.1", + "expect-cookies": "^0.1.2", + "mocha": "^3.2.0", + "nock": "^9.0.13", + "node-rsa": "^0.4.2", + "rimraf": "2.6.1", + "supertest": "^2.0.1" + } +} diff --git a/routes/admin_routes.js b/routes/admin_routes.js index 2713299dd..c0b42a46d 100644 --- a/routes/admin_routes.js +++ b/routes/admin_routes.js @@ -10,7 +10,7 @@ var Camp = require('../models/camp').Camp; var datatableAdmin = require('../libs/admin').datatableAdmin; var adminRender = require('../libs/admin').adminRender; -var sign_up = require('../libs/passport').sign_up; +var sign_up = require('../libs/users').sign_up; router.get('/', userRole.isAdmin(), function (req, res) { adminRender(req, res, 'admin/home.jade', { diff --git a/routes/api_routes.js b/routes/api_routes.js index 721731efc..989474741 100644 --- a/routes/api_routes.js +++ b/routes/api_routes.js @@ -1,61 +1,22 @@ -const request = require('superagent'); const _ = require('lodash'); -let apiTokensConfig = require('config').get('api_tokens') -const userDetailsFromDrupal = (data) => { - return { - uid : _.get(data, 'uid', -1), - name : _.get(data, 'name', ''), - firstname : _.get(data, 'field_profile_first.und.0.value', ''), - lastname : _.get(data, 'field_profile_last.und.0.value', ''), - phone : _.get(data, 'field_profile_phone.und.0.value', -1), - } -}; -module.exports = function (app) { + +module.exports = (app, auth) => { /** - * API: (POST) - * request => /api/userlogin - * params => username, password, token - * usage sample => curl --data "username=Profile_Username&password=Profile_Password&token=Secret_Token" http://localhost:3000/api/userlogin - */ - app.post('/api/userlogin', (req, res) => { - const { username, password, token } = _.get(req, 'body', {username: '', password: '', token: ''}); - if (apiTokensConfig.token !== token) { - res.status(401) - .jsonp({ - status: 'false', - message: 'Invalid Token!', - }); - return; + * API => [POST] /api/userlogin + * params => username, password + * usage sample => curl --data "username=Profile_Username&password=Profile_Password&token=Secret_Token" http://localhost:3000/api/userlogin + */ + app.post(['/jwt-login', '/:lng/login'], (req, res) => { + console.log('/jwt-login'); + if (_.has(req, 'sparkSession')) { + res.sendStatus(200); + } else { + const {username, password} = _.get(req, 'body', {username: '', password: ''}); + return auth.login(username, password) + .then(token => res.cookie(auth.SessionCookieName, token, {maxAge: 60 * 60 * 1000, httpOnly: true}) + .sendStatus(200)) + .catch(() => res.sendStatus(401)); } - - request - .post('https://profile.midburn.org/api/user/login') - .set('Content-Type', 'application/x-www-form-urlencoded') - .set('Accept', 'application/json') - .send({ username, password }) - .then( - ({body}) => { - const { user, sessid } = body; - const { uid, name, firstname, lastname, phone } = userDetailsFromDrupal(user); - res.status(200) - .jsonp({ - status : 'true', - message : 'user authorized', - uid : uid, - username : name, - token : sessid, - firstname : firstname, - lastname : lastname, - phone : phone, - }) - }, - (error) => res.status(401) - .jsonp({ - status: 'false', - message: 'Not authorized!', - error - }) - ); }); }; diff --git a/routes/dev_routes.js b/routes/dev_routes.js index 0f0d17705..0c5d350d2 100644 --- a/routes/dev_routes.js +++ b/routes/dev_routes.js @@ -1,16 +1,15 @@ -var express = require('express'); -var router = express.Router({ +const express = require('express'); +const router = express.Router({ mergeParams: true }); - -var User = require('../models/user').User; +const {User} = require('../models/user'); router.get('/', function (req, res) { res.render('dev_tools/dev_home'); }); router.get('/create-admin', function (req, res) { - var newUser = new User({ + const newUser = new User({ email: 'a', first_name: 'Development', last_name: 'Admin', @@ -21,11 +20,9 @@ router.get('/create-admin', function (req, res) { }); newUser.generateHash('a'); - newUser.save().then(function (model) { - res.redirect("/"); - }); - - res.redirect("./"); + return newUser.save() + .then(() => res.redirect('/')) + .catch(() => res.redirect('./')); }); router.get('/view-debug/*', function (req, res) { diff --git a/routes/main_routes.js b/routes/main_routes.js index a25265244..08b57976c 100644 --- a/routes/main_routes.js +++ b/routes/main_routes.js @@ -11,7 +11,7 @@ var mail = require('../libs/mail'); var log = require('../libs/logger.js')(module); var User = require('../models/user').User; var userRole = require('../libs/user_role'); -var passportLib = require('../libs/passport'); +// var passportLib = require('../libs/passport'); var async = require('async'); var crypto = require('crypto'); @@ -58,102 +58,95 @@ module.exports = function (app, passport) { // ===================================== // LOGIN =============================== // ===================================== - var loginPost = function (req, res, next) { - if (req.body.email.length === 0 || req.body.password.length === 0) { - return res.render('pages/login', { - errorMessage: i18next.t('invalid_user_password') - }); - } - - passport.authenticate('local-login', { - failureFlash: true - }, function (err, user, info) { - if (err) { - return res.render('pages/login', { - errorMessage: err.message - }); - } - - if (!user) { - return res.render('pages/login', { - errorMessage: req.flash('error') - }); - } - return req.logIn(user, function (err) { - if (err) { - return res.render('pages/login', { - errorMessage: req.flash('error') - }); - } else { - res.header('token', passportLib.generateJwtToken(req.body.email)); - var r = req.body['r']; - if (r) { - return res.redirect(r); - } else { - return res.redirect('home'); - } - } - }); - })(req, res, next); - }; - - // process the login form - app.post('/:lng/login', loginPost); + // var loginPost = function (req, res, next) { + // console.log('login render !!!'); + // // if (req.body.email.length === 0 || req.body.password.length === 0) { + // // return res.render('pages/login', { + // // errorMessage: i18next.t('invalid_user_password') + // // }); + // // } + // // return res.render('pages/login', { + // // errorMessage: req.flash('error') + // // }); + // // passport.authenticate('local-login', { + // // failureFlash: true + // // }, function (err, user, info) { + // // if (err) { + // // return res.render('pages/login', { + // // errorMessage: err.message + // // }); + // // } + // // + // // if (!user) { + // // return res.render('pages/login', { + // // errorMessage: req.flash('error') + // // }); + // // } + // // return req.logIn(user, function (err) { + // // if (err) { + // // return res.render('pages/login', { + // // errorMessage: req.flash('error') + // // }); + // // } else { + // // res.header('token', passportLib.generateJwtToken(req.body.email)); + // // var r = req.body['r']; + // // if (r) { + // // return res.redirect(r); + // // } else { + // // return res.redirect('home'); + // // } + // // } + // // }); + // // })(req, res, next); + // }; + + // // process the login form + // app.post('/:lng/login', loginPost); // show the login form app.get('/:lng/login', function (req, res) { - var r = req.query.r; + console.log('GET login render !!!'); + const redirectUrl = req.query.r; + try { res.render('pages/login', { - errorMessage: req.flash('error'), - r: r + errorMessage: i18next.t('invalid_user_password'), + r: redirectUrl }); + + } catch (err) { + console.log(err); + } }); // ===================================== // OAuth =============================== // ===================================== - app.get('/auth/facebook', - passport.authenticate('facebook', { - scope: ['email'] - })); - - app.get('/auth/facebook/reauth', - passport.authenticate('facebook', { - authType: 'rerequest', - scope: ['email'] - })); - - app.get('/auth/facebook/callback', - passport.authenticate('facebook', { - failureRedirect: '/' - }), - function (req, res, c) { - // Successful authentication, redirect home. - res.redirect('/'); - }); - - // ===================================== - // JWT ================================= - // ===================================== - // JWT Login route - app.post("/jwt-login", function (req, res) { - passportLib.login(req.body.email, req.body.password, function(result, user, error) { - if (result && user) { - var token = passportLib.generateJwtToken(req.body.email); - res.json({message: "ok", token: token}); - } - else { - res.status(401).json({message: error}); - } - }); - }); + // app.get('/auth/facebook', + // passport.authenticate('facebook', { + // scope: ['email'] + // })); + // + // app.get('/auth/facebook/reauth', + // passport.authenticate('facebook', { + // authType: 'rerequest', + // scope: ['email'] + // })); + // + // app.get('/auth/facebook/callback', + // passport.authenticate('facebook', { + // failureRedirect: '/' + // }), + // function (req, res, c) { + // // Successful authentication, redirect home. + // res.redirect('/'); + // }); // ===================================== // SIGNUP ============================== // ===================================== var _renderSignup = function (res, req) { return res.render('pages/signup', { - errorMessage: req.flash('error'), + errorMessage: 'some error'/*req.flash('error')*/, body: req.body, //repopulate fields in case of error recaptcha_sitekey: recaptchaConfig.sitekey }); diff --git a/tests/drivers/auth-test-support.js b/tests/drivers/auth-test-support.js new file mode 100644 index 000000000..9958a3ff7 --- /dev/null +++ b/tests/drivers/auth-test-support.js @@ -0,0 +1,34 @@ +const jwt = require('jwt-simple'); +const config = require('config'); +const apiTokensConfig = config.get('api_tokens'); +const Chance = require('chance'); +const chance = new Chance(); +const {SessionCookieName, TokenTTL, TokenRenewalPeriod} = require('../../libs/auth')(); + +const UserLoginUrl = '/jwt-login'; +const TestValidCredentials = {username: 'a', password: 'a'}; +const TestInvalidCredentials = {username: chance.word(), password: chance.word()}; + +const withSessionCookie = sessionToken => [`${SessionCookieName}=${sessionToken}`]; +const withSessionHeader = sessionToken => [`JWT ${sessionToken}`]; + +const randomSessionWithExpiration = timestamp => { + return { + email: chance.email(), + name: chance.word(), + uid: chance.natural(), + exp: timestamp + } +}; + +const randomSession = () => randomSessionWithExpiration(Date.now() + (60 * 1000)); + +const generateSessionCookie = () => withSessionCookie(jwt.encode(randomSession(), apiTokensConfig.token)); +const generateSessionHeader = () => withSessionHeader(jwt.encode(randomSession(), apiTokensConfig.token)); +const generateSessionCookieWithExpiration = timestamp => withSessionCookie(jwt.encode(randomSessionWithExpiration(timestamp), apiTokensConfig.token)); + +const InvalidTokenCookie = withSessionCookie(jwt.encode(randomSession(), chance.word())); +const ExpiredTokenCookie = generateSessionCookieWithExpiration(Date.now() - (TokenTTL + TokenRenewalPeriod + 1)); +const RenewableTokenCookie = generateSessionCookieWithExpiration(Date.now() - (TokenTTL + TokenRenewalPeriod - 2000)); + +module.exports = {withSessionCookie, generateSessionCookie, generateSessionHeader, generateSessionCookieWithExpiration, InvalidTokenCookie, ExpiredTokenCookie, TestValidCredentials, TestInvalidCredentials, RenewableTokenCookie, UserLoginUrl}; diff --git a/tests/libs/security-token-test-support.js b/tests/libs/security-token-test-support.js deleted file mode 100644 index cfd4c1573..000000000 --- a/tests/libs/security-token-test-support.js +++ /dev/null @@ -1,16 +0,0 @@ -const NodeRSA = require('node-rsa'); - -const data = {token: 'data-token'}; -const encryptionKeys = keyPair(); -const anotherEncryptionKeys = keyPair(); - -function keyPair() { - const key = new NodeRSA({b: 512}); - return { - private: key.exportKey('private'), - public: key.exportKey('public') - }; -} - -module.exports = {data, encryptionKeys, anotherEncryptionKeys}; - diff --git a/tests/libs/security-token.test.js b/tests/libs/security-token.test.js deleted file mode 100644 index fa9cdaf07..000000000 --- a/tests/libs/security-token.test.js +++ /dev/null @@ -1,64 +0,0 @@ -const securityToken = require('../../libs/security-token'); -const NodeRSA = require('node-rsa'); -const expect = require('chai').expect; -const securityTokenTestSupport = require('./security-token-test-support'); - -const encryptionKeys = securityTokenTestSupport.encryptionKeys; -const anotherEncryptionKeys = securityTokenTestSupport.anotherEncryptionKeys; - -describe('jwt-crypto', () => { - - it('should encrypt/decrypt a token', () => { - const data = {token: 'data-token'}; - - const encrypted = securityToken.encrypt(data, {privateKey: encryptionKeys.private}); - - expect(securityToken.decrypt(encrypted, {publicKey: encryptionKeys.public})).to.be.deep.property('token', 'data-token'); - }); - - it('should fail to decrypt with non-matching keys', () => { - const data = {token: 'data-token'}; - - const encrypted = securityToken.encrypt(data, {privateKey: encryptionKeys.private}); - - expect(() => securityToken.decrypt(encrypted, {publicKey: anotherEncryptionKeys.public})).to.throw(Error); - }); - - describe('decrypt', () => { - it('should fail if options is not provided', () => { - expect(() => securityToken.decrypt('data')).to.throw('options.publicKey is mandatory'); - }); - - it('should fail if options.publicKey is not provided', () => { - expect(() => securityToken.decrypt('data', {})).to.throw('options.publicKey is mandatory'); - }); - - it('should not validate expiration given "ignoreExpiration" is set', () => { - const minus30Sec = Math.floor(Date.now() / 1000) - 30; - const data = {token: 'data-token', iat: minus30Sec, exp: 10}; - - const encrypted = securityToken.encrypt(data, {privateKey: encryptionKeys.private}); - - expect(() => securityToken.decrypt(encrypted, {publicKey: encryptionKeys.public, ignoreExpiration: true})).to.not.throw(Error); - }); - - it('should validate expiration by default', () => { - const minus30Sec = Math.floor(Date.now() / 1000) - 30; - const data = {token: 'data-token', iat: minus30Sec, exp: 10}; - - const encrypted = securityToken.encrypt(data, {privateKey: encryptionKeys.private}); - - expect(() => securityToken.decrypt(encrypted, {publicKey: encryptionKeys.public})).to.throw(Error); - }); - }); - - describe('encrypt', () => { - it('should fail if options is not provided', () => { - expect(() => securityToken.encrypt('data')).to.throw('options.privateKey is mandatory'); - }); - - it('should fail if options.publicKey is not provided', () => { - expect(() => securityToken.encrypt('data', {})).to.throw('options.privateKey is mandatory'); - }); - }); -}); diff --git a/tests/routes/admin_routes.test.js b/tests/routes/admin_routes.test.js index da0624dea..82205447b 100644 --- a/tests/routes/admin_routes.test.js +++ b/tests/routes/admin_routes.test.js @@ -5,6 +5,7 @@ var DrupalUser = require('../../models/user').DrupalUser; var User = require('../../models/user').User; var knex = require('../../libs/db').knex; var constants = require('../../models/constants'); +var {generateSessionCookie} = require('../drivers/auth-test-support'); var _ = require('lodash'); const ADMIN_USER_EMAIL = "omerpines@hotmail.com"; @@ -57,14 +58,17 @@ var givenAdminUserIsLoggedIn = function() { }; var adminHomeShouldShowSomeData = function() { - return request.get('/he/admin').expect(200).expect(function(res) { - if ( - (res.text.indexOf("Total Users") < 0) || - (res.text.indexOf("Total Camps") < 0) - ) { - throw new Error(); - } - }); + return request.get('/he/admin') + .set('Cookie', [generateSessionCookie()]) + .expect(200) + .expect(function(res) { + if ( + (res.text.indexOf("Total Users") < 0) || + (res.text.indexOf("Total Camps") < 0) + ) { + throw new Error(); + } + }); }; var givenUserAdminTableAjaxUrl = function() { @@ -132,7 +136,8 @@ var shouldChangeAdminUserLastNameTo = function(last_name) { describe('Admin routes', function() { it('should show some statistical data on admin homepage', function() { this.timeout(5000); - return givenAdminUserIsLoggedIn().then(adminHomeShouldShowSomeData); + return adminHomeShouldShowSomeData(); + // givenAdminUserIsLoggedIn().then(adminHomeShouldShowSomeData); }); // it('should show admin user in users table', function() { @@ -148,4 +153,4 @@ describe('Admin routes', function() { .then(_.partial(givenAdminUserLastNameIs, 'foobar')) .then(_.partial(shouldChangeAdminUserLastNameTo, 'bazbax')); }) -}); \ No newline at end of file +}); diff --git a/tests/routes/api_routes.test.js b/tests/routes/api_routes.test.js index 4885de528..4ea887a1e 100644 --- a/tests/routes/api_routes.test.js +++ b/tests/routes/api_routes.test.js @@ -1,49 +1,72 @@ // This is magically used in code such as user.attributes.password.length.should.be.above(20); -var should = require('chai').should(); // eslint-disable-line no-unused-vars - -var app = require('../../app'); -var request = require('supertest')(app); -var User = require('../../models/user').User; -var knex = require('../../libs/db').knex; +const {should, expect} = require('chai'); // eslint-disable-line no-unused-vars +const Cookies = require('expect-cookies'); +const app = require('../../app'); +const request = require('supertest')(app); const assert = require('assert'); -const TEST_TOKEN = "YWxseW91bmVlZGlzbG92ZWFsbHlvdW5lZWRpc2xvdmVsb3ZlbG92ZWlzYWxseW91"; +const {InvalidTokenCookie, ExpiredTokenCookie, RenewableTokenCookie, TestValidCredentials, TestInvalidCredentials, UserLoginUrl, withSessionCookie, generateSessionCookie, generateSessionHeader} = require('../drivers/auth-test-support'); +const {SessionCookieName} = require('../../libs/auth')(); + describe('API routes', function() { - it('should reject with no token', function() { - return request - .post('/api/userlogin') - .expect(401); - }); - - it('should reject with invalid token', function() { - return request - .post('/api/userlogin') - .send({ - token: "INVALID" - }) - .expect(401); - }); - - it('should reject with invalid login', function() { - return request - .post('/api/userlogin') - .send({ - username: "none", - password: "invalid", - token: TEST_TOKEN - }) - .expect(401) - }); - - it('should login', function() { - return request - .post('/api/userlogin') - .send({ - username: "omerpines@hotmail.com", - password: "123456", - token: TEST_TOKEN - }) - .expect(200) - }); - -}); \ No newline at end of file + + before(() => request.get('/dev/create-admin')); + + it('should reject with no token', () => + request.post(UserLoginUrl) + .expect(401)); + + it('should reject with invalid login', () => + request.post(UserLoginUrl) + .send(TestInvalidCredentials) + .expect(401)); + + it('should reject with invalid token', () => + request.post(UserLoginUrl) + .set('Cookie', InvalidTokenCookie) + .expect(401)); + + it('should reject with expired token', () => + request.post(UserLoginUrl) + .set('Cookie', ExpiredTokenCookie) + .expect(401)); + + it('should automatically renew token is expired within a specific time range', () => + request.post(UserLoginUrl) + .set('Cookie', RenewableTokenCookie) + .expect(200) + .then(res => { + const sessionCookie = res.headers['set-cookie'][0]; + expect(sessionCookie).to.match(/^spark_session=/); + const cookieProperties = sessionCookie.split(';').map(entry => entry.split('=')[0].trimLeft().toLowerCase()); + expect(cookieProperties).to.include.members(['path', 'httponly', 'max-age']); + }) ); + + it('should accept with valid auth header', () => + request.post(UserLoginUrl) + .set({'authorization': generateSessionHeader()}) + .expect(200)); + + it('should set cookie when login is successful', () => + request.post(UserLoginUrl) + .send(TestValidCredentials) + .expect(200) + .expect(Cookies.new({'name': SessionCookieName, 'options': ['path', 'httponly', 'max-age']}))); + + it('should be not redirect to login in case token exists', () => + request.post(UserLoginUrl) + .set('Cookie', generateSessionCookie()) + .expect(200)); + + it('should return 200OK if encountered valid token', () => + request.post(UserLoginUrl) + .send(TestValidCredentials) + .then(res => { + if (res.status !== 200) assert.fail(res.status, 200, "login failed"); + const sessionToken = res.headers['set-cookie'][0].split(';')[0].split('=')[1]; + console.log(`login with token [${sessionToken}]`); + return request.post(UserLoginUrl) + .set('Cookie', withSessionCookie(sessionToken)) + .expect(200); + })); +}); diff --git a/tests/routes/main_routes.test.js b/tests/routes/main_routes.test.js index 8c65a81b4..7038a2ede 100644 --- a/tests/routes/main_routes.test.js +++ b/tests/routes/main_routes.test.js @@ -6,14 +6,14 @@ var request = require('supertest')(app); var DrupalUser = require('../../models/user').DrupalUser; var User = require('../../models/user').User; var knex = require('../../libs/db').knex; +const {generateSessionCookie} = require('../drivers/auth-test-support'); describe('Main routes', function() { this.timeout(5000); it('responds to / with redirect to hebrew', function testSlash(done) { - request - .get('/') - .expect('Location', '/he/login?r=/') - .expect(302, done); + request.get('/') + .expect('Location', '/he/login?r=/') + .expect(302, done); }); it('greets in Hebrew', function testSlash(done) { @@ -47,13 +47,13 @@ describe('Main routes', function() { }); it('returns 404 MOOP! on everything else', function testPath(done) { - request - .get('/foo/bar') + request.get('/foo/bar') + .set('Cookie', [generateSessionCookie()]) .expect(/<[Hh]1>MOOP!<\/[Hh]1>/) .expect(404, done); }); - it('redirects to facebook on facebok login', function facebookRedirect(done) { + it.skip('redirects to facebook on facebok login', function facebookRedirect(done) { request .get('/auth/facebook') .expect('Location', /https:\/\/www\.facebook\.com\/dialog\/oauth\?response_type=code&redirect_uri=/) @@ -94,4 +94,4 @@ describe('Main routes', function() { }); }).then(done); }); -}); \ No newline at end of file +}); diff --git a/tests/routes/volunteers_routes.test.js_disabled b/tests/routes/volunteers_routes.test.js_disabled index e7b936c1a..8463fa9dd 100644 --- a/tests/routes/volunteers_routes.test.js_disabled +++ b/tests/routes/volunteers_routes.test.js_disabled @@ -3,6 +3,7 @@ const should = require('chai').should(); const request = require('supertest')(app); const nock = require('nock'); const profilesApi = require('config').get('profiles_api'); +const {generateSessionCookie} = require('../drivers/auth-test-support'); const testUser1 = { id: 1, @@ -56,6 +57,7 @@ describe('Getters all respond', function() { it('returns roles', function getRoles(done) { request .get('/volunteers/roles/') + .set('Cookie', [generateSessionCookie()]) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -79,6 +81,7 @@ describe('Getters all respond', function() { it('returns departments', function getDeprtments(done) { request .get('/volunteers/departments/') + .set('Cookie', [generateSessionCookie()]) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -106,6 +109,7 @@ describe('Getters all respond', function() { it('returns all volunteers with expected structure', function(done) { request .get('/volunteers/volunteers') + .set('Cookie', [generateSessionCookie()]) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -137,6 +141,7 @@ describe('Getters all respond', function() { it('returns volunteers of department 1', function(done) { request .get('/volunteers/departments/1/volunteers') + .set('Cookie', [generateSessionCookie()]) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -179,6 +184,7 @@ describe('Adding volunteers', function() { setupDrupalMock(1); request .post(`/volunteers/departments/${departmentId}/volunteers/`) + .set('Cookie', [generateSessionCookie()]) .type('application/json') .accept('application/json') .send(JSON.stringify( @@ -204,6 +210,7 @@ describe('Adding volunteers', function() { request .post(`/volunteers/departments/${departmentId}/volunteers/`) + .set('Cookie', [generateSessionCookie()]) .type('application/json') .accept('application/json') .send( //JSON.stringify( @@ -222,7 +229,8 @@ describe('Adding volunteers', function() { setupDrupalMock(1); request - .get(`/volunteers/departments/${departmentId}/volunteers`) //TODO change back to dep 3 + .get(`/volunteers/departments/${departmentId}/volunteers`) //TODO change back to dep 3 + .set('Cookie', [generateSessionCookie()]) .expect('Content-Type', /json/) .expect(200) .end(function(err, res) { @@ -247,7 +255,7 @@ describe('Adding volunteers', function() { //extra fields are rejected //missing fields are ignored or rejected //length and type limits on mail and numbers - //wrong format + //wrong format }); @@ -258,7 +266,7 @@ describe.skip('Volunteers editing', function() { //extra fields are rejected //missing fields are ignored or rejected //length and type limits on mail and numbers - //wrong format + //wrong format }); @@ -267,4 +275,4 @@ describe('Volunteers deletion', function() { //get and delete and get //delete non existing //delete deos not affect other departments -}); \ No newline at end of file +}); diff --git a/yarn.lock b/yarn.lock index a8f701025..b4541c46d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -182,7 +182,7 @@ async@^0.9.0: version "0.9.2" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" -async@^1.3.0, async@^1.5.2: +async@^1.3.0, async@^1.5.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -672,6 +672,10 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chance@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/chance/-/chance-1.0.6.tgz#4734f62d02b738cdc2882d8b5d41f89af49e7bfd" + character-parser@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-1.2.1.tgz#c0dde4ab182713b919b970959a123ecc1a30fcd6" @@ -857,7 +861,7 @@ cookie-parser@~1.4.3: cookie "0.3.1" cookie-signature "1.0.6" -cookie-signature@1.0.6: +cookie-signature@1.0.6, cookie-signature@1.0.x: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1396,6 +1400,13 @@ expand-tilde@^1.2.2: dependencies: os-homedir "^1.0.1" +expect-cookies@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/expect-cookies/-/expect-cookies-0.1.2.tgz#1ac7374fe46d8847dc9e6e1811467707bd94f9fe" + dependencies: + cookie-signature "1.0.x" + should "7.1.x" + express-breadcrumbs@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/express-breadcrumbs/-/express-breadcrumbs-0.0.2.tgz#3e1c6804c1884cc7917a725743860d4953c13798" @@ -1420,6 +1431,15 @@ express-fileupload@0.0.5: fs-extra "^0.22.1" streamifier "^0.1.1" +express-jwt@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/express-jwt/-/express-jwt-5.3.0.tgz#3d90cd65802e6336252f19e6a3df3e149e0c5ea0" + dependencies: + async "^1.5.0" + express-unless "^0.3.0" + jsonwebtoken "^7.3.0" + lodash.set "^4.0.0" + express-mailer@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/express-mailer/-/express-mailer-0.3.1.tgz#c94bee5a5d287bd7a6db4c8d46634e2d1fae9037" @@ -1444,6 +1464,10 @@ express-session@^1.14.0: uid-safe "~2.1.3" utils-merge "1.0.0" +express-unless@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/express-unless/-/express-unless-0.3.0.tgz#5c795e7392571512dd28f520b3857a52b21261a2" + express@~4.13.4: version "4.13.4" resolved "https://registry.yarnpkg.com/express/-/express-4.13.4.tgz#3c0b76f3c77590c8345739061ec0bd3ba067ec24" @@ -2352,6 +2376,10 @@ jws@^3.1.4: jwa "^1.1.4" safe-buffer "^5.0.1" +jwt-simple@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.1.tgz#79ea01891b61de6b68e13e67c0b4b5bda937b294" + keygrip@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.1.tgz#b02fa4816eef21a8c4b35ca9e52921ffc89a30e9" @@ -2496,6 +2524,10 @@ lodash.pickby@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" +lodash.set@^4.0.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" + lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.6.0, lodash@~4.17.2: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -2700,11 +2732,11 @@ morgan@~1.7.0: on-finished "~2.3.0" on-headers "~1.0.1" -ms@0.7.1: +ms@0.7.1, ms@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" -ms@0.7.2, ms@^0.7.1: +ms@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765" @@ -3498,11 +3530,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" - -rimraf@2.6.1: +rimraf@2, rimraf@2.6.1, rimraf@^2.2.8: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: @@ -3641,6 +3669,30 @@ shelljs@^0.7.5: interpret "^1.0.0" rechoir "^0.6.2" +should-equal@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-0.5.0.tgz#c797f135f3067feb69ebecdb306b1c3fe21b3e6f" + dependencies: + should-type "0.2.0" + +should-format@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/should-format/-/should-format-0.3.1.tgz#2cbb782461670ace4292b2b1ec468db8cf99e330" + dependencies: + should-type "0.2.0" + +should-type@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/should-type/-/should-type-0.2.0.tgz#6707ef95529d989dcc098fe0753ab1f9136bb7f6" + +should@7.1.x: + version "7.1.1" + resolved "https://registry.yarnpkg.com/should/-/should-7.1.1.tgz#6464c48b6f7c1e1f18ac0483578fa2dd55c2c6e0" + dependencies: + should-equal "0.5.0" + should-format "0.3.1" + should-type "0.2.0" + sigmund@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"