Files
talk/services/passport.js
T
2017-04-05 11:39:54 -06:00

362 lines
10 KiB
JavaScript

const passport = require('passport');
const UsersService = require('./users');
const SettingsService = require('./settings');
const fetch = require('node-fetch');
const FormData = require('form-data');
const LocalStrategy = require('passport-local').Strategy;
const errors = require('../errors');
const debug = require('debug')('talk:passport');
//==============================================================================
// SESSION SERIALIZATION
//==============================================================================
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
UsersService
.findById(id)
.then((user) => {
done(null, user);
})
.catch((err) => {
done(err);
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Validates that a user is allowed to login.
* @param {User} user the user to be validated
* @param {Function} done the callback for the validation
*/
function ValidateUserLogin(loginProfile, user, done) {
if (!user) {
return done(new Error('user not found'));
}
if (user.disabled) {
return done(new errors.ErrAuthentication('Account disabled'));
}
// If the user isn't a local user (i.e., a social user).
if (loginProfile.provider !== 'local') {
return done(null, user);
}
// The user is a local user, check if we need email confirmation.
return SettingsService.retrieve().then(({requireEmailConfirmation = false}) => {
// If we have the requirement of checking that emails for users are
// verified, then we need to check the email address to ensure that it has
// been verified.
if (requireEmailConfirmation) {
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === loginProfile.id);
// This should never get to this point, if it does, don't let this past.
if (!profile) {
throw new Error('ID indicated by loginProfile is not on user object');
}
// If the profile doesn't have a metadata field, or it does not have a
// confirmed_at field, or that field is null, then send them back.
if (!profile.metadata || !profile.metadata.confirmed_at || profile.metadata.confirmed_at === null) {
return done(new errors.ErrAuthentication(loginProfile.id));
}
}
return done(null, user);
});
}
//==============================================================================
// STRATEGIES
//==============================================================================
/**
* This looks at the request headers to see if there is a recaptcha response on
* the input request.
*/
const CheckIfRecaptcha = (req) => {
let response = req.get('X-Recaptcha-Response');
if (response && response.length > 0) {
return true;
}
return false;
};
/**
* This checks the user to see if the current email profile needs to get checked
* for recaptcha compliance before being allowed to login.
*/
const CheckIfNeedsRecaptcha = (user, email) => {
// Get the profile representing the local account.
let profile = user.profiles.find((profile) => profile.id === email);
// This should never get to this point, if it does, don't let this past.
if (!profile) {
throw new Error('ID indicated by loginProfile is not on user object');
}
if (profile.metadata && profile.metadata.recaptcha_required) {
return true;
}
return false;
};
/**
* This stores the Recaptcha secret.
*/
const RECAPTCHA_SECRET = process.env.TALK_RECAPTCHA_SECRET;
const RECAPTCHA_PUBLIC = process.env.TALK_RECAPTCHA_PUBLIC;
/**
* This is true when the recaptcha secret is provided and the Recaptcha feature
* is to be enabled.
*/
const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0 && RECAPTCHA_PUBLIC && RECAPTCHA_PUBLIC.length > 0;
if (!RECAPTCHA_ENABLED) {
console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.');
}
/**
* This sends the request details down Google to check to see if the response is
* genuine or not.
* @return {Promise} resolves with the success status of the recaptcha
*/
const CheckRecaptcha = async (req) => {
// Ask Google to verify the recaptcha response: https://developers.google.com/recaptcha/docs/verify
const form = new FormData();
form.append('secret', RECAPTCHA_SECRET);
form.append('response', req.get('X-Recaptcha-Response'));
form.append('remoteip', req.ip);
// Perform the request.
let res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
body: form,
headers: form.getHeaders()
});
// Parse the JSON response.
let json = await res.json();
return json.success;
};
/**
* This records a login attempt failure as well as optionally flags an account
* for requiring a recaptcha in the future outside the temporary window.
* @return {Promise} resolves with nothing if rate limit not exeeded, errors if
* there is a rate limit error
*/
const HandleFailedAttempt = async (email, userNeedsRecaptcha) => {
try {
await UsersService.recordLoginAttempt(email);
} catch (err) {
if (err === errors.ErrLoginAttemptMaximumExceeded && !userNeedsRecaptcha && RECAPTCHA_ENABLED) {
debug(`flagging user email=${email}`);
await UsersService.flagForRecaptchaRequirement(email, true);
}
throw err;
}
};
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, async (req, email, password, done) => {
// We need to check if this request has a recaptcha on it at all, if it does,
// we must verify it first. If verification fails, we fail the request early.
// We can only do this obviously when recaptcha is enabled.
let hasRecaptcha = CheckIfRecaptcha(req);
let recaptchaPassed = false;
if (RECAPTCHA_ENABLED && hasRecaptcha) {
try {
// Check to see if this recaptcha passed.
recaptchaPassed = await CheckRecaptcha(req);
} catch (err) {
return done(err);
}
if (!recaptchaPassed) {
try {
await HandleFailedAttempt(email);
} catch (err) {
return done(err);
}
return done(null, false, {message: 'Incorrect recaptcha'});
}
}
debug(`hasRecaptcha=${hasRecaptcha}, recaptchaPassed=${recaptchaPassed}`);
// If the request didn't have a recaptcha, check to see if we did need one by
// checking the rate limit against failed attempts on this email
// address/login.
if (!hasRecaptcha) {
try {
await UsersService.checkLoginAttempts(email);
} catch (err) {
if (err === errors.ErrLoginAttemptMaximumExceeded) {
// This says, we didn't have a recaptcha, yet we needed one.. Reject
// here.
try {
await HandleFailedAttempt(email);
} catch (err) {
return done(err);
}
return done(null, false, {message: 'Incorrect recaptcha'});
}
// Some other unexpected error occured.
return done(err);
}
}
// Let's find the user for which this login is connected to.
let user;
try {
user = await UsersService.findLocalUser(email);
} catch (err) {
return done(err);
}
debug(`user=${user != null}`);
// If the user doesn't exist, then mark this as a failed attempt at logging in
// this non-existant user and continue.
if (!user) {
try {
await HandleFailedAttempt(email);
} catch (err) {
return done(err);
}
return done(null, false, {message: 'Incorrect email/password combination'});
}
// Let's check if the user indeed needed recaptcha in order to authenticate.
// We can only do this obviously when recaptcha is enabled.
let userNeedsRecaptcha = false;
if (RECAPTCHA_ENABLED && user) {
userNeedsRecaptcha = CheckIfNeedsRecaptcha(user, email);
}
debug(`userNeedsRecaptcha=${userNeedsRecaptcha}`);
// Let's check now if their password is correct.
let userPasswordCorrect;
try {
userPasswordCorrect = await user.verifyPassword(password);
} catch (err) {
return done(err);
}
debug(`userPasswordCorrect=${userPasswordCorrect}`);
// If their password wasn't correct, mark their attempt as failed and
// continue.
if (!userPasswordCorrect) {
try {
await HandleFailedAttempt(email, userNeedsRecaptcha);
} catch (err) {
return done(err);
}
return done(null, false, {message: 'Incorrect email/password combination'});
}
// If the user needed a recaptcha, yet we have gotten this far, this indicates
// that the password was correct, so let's unflag their account for logins. We
// can only do this obviously when recaptcha is enabled. The account wouldn't
// have been flagged otherwise.
if (RECAPTCHA_ENABLED && userNeedsRecaptcha) {
try {
await UsersService.flagForRecaptchaRequirement(email, false);
} catch (err) {
return done(err);
}
}
// Define the loginProfile being used to perform an additional
// verificaiton.
let loginProfile = {id: email, provider: 'local'};
// Perform final steps to login the user.
return ValidateUserLogin(loginProfile, user, done);
}));
module.exports = {
passport,
ValidateUserLogin,
HandleFailedAttempt,
HandleAuthCallback,
HandleAuthPopupCallback
};