Strider-CD/strider

View on GitHub
apps/strider/lib/auth.js

Summary

Maintainability
F
2 wks
Test Coverage
const crypto = require('crypto');
const BluebirdPromise = require('bluebird');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const utils = require('./utils');
const mailer = require('./email');
require('./config');
require('./logging');
const User = require('./models').User;
const randomBytes = BluebirdPromise.promisify(crypto.randomBytes);

function setupPasswordAuth() {
  passport.use(
    new LocalStrategy(
      {
        usernameField: 'email',
      },
      function (username, password, done) {
        console.log('username: %s', username);

        User.authenticate(username, password, function (err, user) {
          if (err || !user) {
            console.log('no user');
            return done(null, false, { message: 'Incorrect username.' });
          }

          return done(null, user);
        });
      }
    )
  );
}

function registerRoutes(app) {
  app.get('/register', function (req, res, next) {
    if (req.query.ember) {
      return next();
    }
    return res.redirect('/register?ember=true');
  });

  app.post('/register', function (req, res) {
    const errors = [];

    if (!req.body.inviteCode) errors.push('No invite code specified');
    if (!req.body.email) errors.push('Missing email');
    if (!req.body.password) errors.push('Missing password');

    if (errors.length) {
      return res.status(400).json({ errors: errors });
    }

    User.registerWithInvite(
      req.body.invite_code,
      req.body.email,
      req.body.password,
      function (err, user) {
        if (err) {
          return res.status(400).json({
            errors: [err],
          });
        }

        // Registered success:
        req.login(user, function () {
          res.redirect('/');
        });
      }
    );
  });

  app.get('/login', function (req, res, next) {
    if (req.user) {
      return res.redirect('/');
    }

    // Pass to Ember if not an error from old login
    if (!req.query.failed && !req.query.ember) {
      return res.redirect('/login?ember=true');
    } else if (!req.query.failed && req.query.ember) {
      return next();
    }

    const failed = Boolean(req.query.failed);
    const errors = [];

    if (failed) {
      errors.push(
        'Authentication failed, please supply a valid email/password.'
      );
    }

    return res.render('login.html', {
      errors: errors,
    });
  });
}

function setup(app) {
  app.registerAuthStrategy = function (strategy) {
    passport.use(strategy);
  };

  app.authenticate = function () {
    console.log('AUTHENTICATE', arguments);

    const res = passport.authenticate.apply(passport, arguments);
    console.log('!!!', res);

    return function (req) {
      console.log(
        '>>>> AUTHENTICATE',
        req._passport,
        req._passport.instance._strategies.github
      );
      res.apply(this, arguments);
    };
  };

  setupPasswordAuth();

  // serialize user on login
  passport.serializeUser(function (user, done) {
    done(null, user.id);
  });

  // deserialize user on logout
  passport.deserializeUser(function (id, done) {
    User.findById(id, function (err, user) {
      done(err, user);
    });
  });

  app.use(passport.initialize());
  app.use(passport.session());
  app.use(basicAuth);

  // Middleware to setup view parameters with user
  app.use(function (req, res, next) {
    if (req.user) {
      req.user.gravatar = utils.gravatar(req.user.email);
    }

    res.locals.currentUser = req.user || null;
    next();
  });

  registerRoutes(app);
}

function basicAuth(req, res, next) {
  const auth = req.get('authorization');
  if (!auth) return next();

  const parts = auth.split(' ');
  if (parts.length !== 2 || parts[0].toLowerCase() !== 'basic') return next();

  let plain;
  try {
    plain = new Buffer(parts[1], 'base64').toString().split(':');
  } catch (e) {
    console.error('Invalid base64 in auth header');
    return next();
  }
  if (plain.length < 2) {
    console.error('Invalid auth header');
    return next();
  }

  User.authenticate(plain[0], plain.slice(1).join(':'), function (err, user) {
    if (err || !user) {
      console.log('basic auth: user not found');
      return res.status(401).send('Invalid username/password in basic auth');
    }

    req.user = user;

    return next();
  });
}

const _authenticate = (cb) => passport.authenticate('local', cb);

function logout(req, res) {
  req.logout();
  res.redirect('/login?ember=true');
}

function forgot(req, res) {
  const email = req.body.email.toLowerCase();

  User.findOne({ email: { $regex: new RegExp(email, 'i') } }, function (
    error,
    user
  ) {
    if (error) {
      return res.status(400).json({
        errors: ['An error occured while attempting to reset your password.'],
      });
    }

    if (user) {
      randomBytes(20)
        .then(function (buffer) {
          return buffer.toString('hex');
        })
        .then(function (token) {
          user.resetPasswordToken = token;
          user.resetPasswordExpires = Date.now() + 3600000; // 1 hour

          return new BluebirdPromise(function (resolve, reject) {
            user.save(function (err, u) {
              if (err) {
                reject(err);
              } else {
                resolve(u);
              }
            });
          });
        })
        .then(function (user) {
          mailer.sendPasswordReset(user);
          res.json({
            ok: true,
            message:
              'Please check your email for the password reset url. Thank you!',
          });
        })
        .catch(function (error) {
          res.status(500).json({ errors: ['Password reset error: ' + error] });
        });
    } else {
      res
        .status(404)
        .json({ errors: ['We could not find a user with that email.'] });
    }
  });
}

function reset(req, res, next) {
  if (req.query.ember) {
    return next();
  }

  const token = req.params.token;

  User.findOne(
    {
      resetPasswordToken: token,
      resetPasswordExpires: { $gt: Date.now() },
    },
    function (err, user) {
      if (!user) {
        return res.status(400).json({
          errors: ['Password reset token is invalid or has expired.'],
        });
      }

      res.render('reset.html', {
        token: token,
        user: user,
      });
    }
  );
}

function resetPost(req, res) {
  const password = req.body.password;
  const confirmation = req.body.passwordConfirmation;

  if (password === confirmation) {
    User.findOne(
      {
        resetPasswordToken: req.params.token,
        resetPasswordExpires: { $gt: Date.now() },
      },
      function (err, user) {
        if (!user) {
          req.flash('error', 'Password reset token is invalid or has expired.');
          return res.redirect('back');
        }

        user.password = password;
        user.resetPasswordToken = undefined;
        user.resetPasswordExpires = undefined;

        user.save(function (err) {
          if (err) {
            req.flash('error', "Couldn't save changes with new password.");
            return res.redirect('/');
          }

          req.login(user, function (err) {
            if (err) {
              req.flash(
                'error',
                "You'r user authentication was not successful."
              );
            }

            res.redirect('/');
          });
        });
      }
    );
  } else {
    res.redirect(req.header('Referer'));
  }
}

// Require a logged in session
function requireUser(req, res, next) {
  if (req.user) {
    next();
  } else {
    req.session.return_to = req.url;
    res.redirect('/login?ember=true');
  }
}

function requireUserOr401(req, res, next) {
  if (req.user) {
    next();
  } else {
    res.status(401).send('not authorized');
  }
}

// Require admin privileges
function requireAdminOr401(req, res, next) {
  if (!req.user || !req.user['account_level'] || req.user.account_level < 1) {
    res.status(401).send('not authorized');
  } else {
    next();
  }
}

// Require the logged-in user to have admin access to the repository in the
// URI path.
// E.g. http://striderapp.com/beyondfog/strider/latest_build
function requireProjectAdmin(req, res, next) {
  if (!req.project) return res.status(404).send('Project not loaded');
  if (!req.user) return res.status(401).send('No user');
  const isAdmin = req.user.account_level && req.user.account_level > 0;
  const notAuthed = (!req.accessLevel || req.accessLevel < 2) && !isAdmin;
  if (notAuthed)
    return res.status(401).send('Not authorized for configuring this project');
  next();
}

module.exports = {
  setup: setup,
  authenticate: _authenticate,
  logout: logout,
  forgot: forgot,
  reset: reset,
  resetPost: resetPost,

  // Auth middleware
  requireUser: requireUser,
  requireUserOr401: requireUserOr401,
  requireAdminOr401: requireAdminOr401,
  requireProjectAdmin: requireProjectAdmin,
};