@perfood/couch-auth
Version:
Easy and secure authentication for CouchDB/Cloudant. Based on SuperLogin, updated and rewritten in Typescript.
380 lines (379 loc) • 16.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const express_slow_down_1 = __importDefault(require("express-slow-down"));
const user_1 = require("./user");
const util_1 = require("./util");
function default_1(config, router, passport, user) {
const env = process.env.NODE_ENV || 'development';
const disabled = config.security.disabledRoutes;
function loginLocal(req, res, next) {
passport.authenticate('local', function (err, user, info) {
if (err) {
return next(err);
}
if (!user) {
// Authentication failed
return res.status(401).json(info);
}
// Success
req.logIn(user, { session: false }, function (err) {
if (err) {
return next(err);
}
});
return next();
})(req, res, next);
}
if (!disabled.includes('login')) {
const speedLimiter = (0, express_slow_down_1.default)({
windowMs: config.security.loginRateLimit?.windowMs || 5 * 60 * 1000,
delayAfter: config.security.loginRateLimit?.delayAfter || 3,
delayMs: (used, req, res) => {
const delay = config.security.loginRateLimit?.delayMs || 500;
return typeof delay === 'function' ? delay(used, req, res) : delay;
},
maxDelayMs: config.security.loginRateLimit?.maxDelayMs || 10000,
skipSuccessfulRequests: config.security.loginRateLimit?.skipSuccessfulRequests || true,
skipFailedRequests: config.security.loginRateLimit?.skipFailedRequests || false,
keyGenerator: function (req) {
const usernameField = config.local.usernameField || 'username';
return req.body[usernameField];
},
store: config.security.loginRateLimit?.store || undefined,
headers: config.security.loginRateLimit?.headers || false
});
router.post('/login', speedLimiter, function (req, res, next) {
loginLocal(req, res, next);
}, function (req, res, next) {
return user
.createSession({
login: req.user._id,
provider: 'local',
byUUID: true,
sessionType: req.body.sessionType
})
.then(function (mySession) {
res.status(200).json(mySession);
}, function (err) {
return next(err);
});
});
}
if (!disabled.includes('refresh'))
router.post('/refresh', passport.authenticate('bearer', { session: false }), function (req, res, next) {
return user.refreshSession(req.user.key).then(function (mySession) {
res.status(200).json(mySession);
}, function (err) {
return next(err);
});
});
if (!disabled.includes('logout'))
router.post('/logout', function (req, res, next) {
const sessionToken = (0, util_1.getSessionToken)(req);
if (!sessionToken) {
return next({
error: 'unauthorized',
status: 401
});
}
user.logoutSession(sessionToken).then(function () {
res.status(200).json({ ok: true, success: 'Logged out' });
}, function (err) {
console.error('Logout failed');
return next(err);
});
});
if (!disabled.includes('logout-others'))
router.post('/logout-others', passport.authenticate('bearer', { session: false }), function (req, res, next) {
user.logoutOthers(req.user.key).then(function () {
res.status(200).json({ success: 'Other sessions logged out' });
}, function (err) {
console.error('Logout failed');
return next(err);
});
});
if (!disabled.includes('logout-all'))
router.post('/logout-all', function (req, res, next) {
const sessionToken = (0, util_1.getSessionToken)(req);
if (!sessionToken) {
return next({
error: 'unauthorized',
status: 401
});
}
user.logoutAll(null, sessionToken).then(function () {
res.status(200).json({ success: 'Logged out' });
}, function (err) {
console.error('Logout-all failed');
return next(err);
});
});
// Setting up the auth api
if (!disabled.includes('register'))
router.post('/register', function (req, res, next) {
user.createUser(req.body, req).then(function (newUser) {
if (!newUser || !config.security.loginOnRegistration) {
res.status(200).json({ success: 'Request processed.' });
}
else if (newUser && config.security.loginOnRegistration) {
return user
.createSession({
login: newUser._id,
provider: 'local',
byUUID: true,
sessionType: req.body.sessionType
})
.then(function (mySession) {
res.status(200).json(mySession);
}, function (err) {
return next(err);
});
}
}, function (err) {
return next(err);
});
});
if (!disabled.includes('forgot-username')) {
router.post('/forgot-username', function (req, res, next) {
user.forgotUsername(req.body.email, req).then(function () {
res.status(200).json({ success: 'Request processed.' });
}, function (err) {
return next(err);
});
});
}
if (!disabled.includes('forgot-password'))
router.post('/forgot-password', function (req, res, next) {
user.forgotPassword(req.body.email, req).then(function () {
res.status(200).json({ success: 'Request processed.' });
}, function (err) {
return next(err);
});
});
if (!disabled.includes('password-reset')) {
const speedLimiter = (0, express_slow_down_1.default)({
windowMs: config.security.passwordResetRateLimit?.windowMs || 5 * 60 * 1000,
delayAfter: config.security.passwordResetRateLimit?.delayAfter || 3,
delayMs: (used, req, res) => {
const delay = config.security.passwordResetRateLimit?.delayMs || 500;
return typeof delay === 'function' ? delay(used, req, res) : delay;
},
maxDelayMs: config.security.passwordResetRateLimit?.maxDelayMs || 10000,
skipSuccessfulRequests: config.security.passwordResetRateLimit?.skipSuccessfulRequests || true,
skipFailedRequests: config.security.passwordResetRateLimit?.skipFailedRequests || false,
keyGenerator: function (req) {
const usernameField = config.local.usernameField || 'username';
return req.body[usernameField];
},
store: config.security.passwordResetRateLimit?.store || undefined,
headers: config.security.passwordResetRateLimit?.headers || false
});
router.post('/password-reset', function (req, res, next) {
if (!config.security.passwordResetRateLimit) {
return next();
}
const usernameField = config.local.usernameField || 'username';
if (!req.body[usernameField]) {
return next({
error: 'username required',
status: 422
});
}
return next();
}, speedLimiter, function (req, res, next) {
user.resetPassword(req.body, req).then(function (currentUser) {
if (config.security.loginOnPasswordReset) {
return user
.createSession({
login: currentUser._id,
provider: 'local',
byUUID: true,
sessionType: req.body.sessionType
})
.then(function (mySession) {
res.status(200).json(mySession);
}, function (err) {
return next(err);
});
}
else {
res.status(200).json({ success: 'Password successfully reset.' });
}
}, function (err) {
return next(err);
});
});
}
if (!disabled.includes('password-change'))
router.post('/password-change', passport.authenticate('bearer', { session: false }), function (req, res, next) {
user.changePasswordSecure(req.user._id, req.body, req).then(function () {
res.status(200).json({ success: 'password changed' });
}, function (err) {
return next(err);
});
});
if (!disabled.includes('unlink'))
router.post('/unlink/:provider', passport.authenticate('bearer', { session: false }), function (req, res, next) {
const provider = req.params.provider;
user.unlinkUserSocial(req.user._id, provider).then(function () {
res.status(200).json({
success: (0, util_1.capitalizeFirstLetter)(provider) + ' unlinked'
});
}, function (err) {
return next(err);
});
});
if (!disabled.includes('confirm-email'))
router.get('/confirm-email/:token', function (req, res, next) {
const redirectURL = config.local.confirmEmailRedirectURL;
if (!req.params.token) {
const err = { error: 'Email verification token required' };
if (redirectURL) {
return res
.status(201)
.redirect(redirectURL + '?error=' + encodeURIComponent(err.error));
}
return res.status(400).send(err);
}
user.verifyEmail(req.params.token).then(function () {
if (redirectURL) {
return res.status(201).redirect(redirectURL + '?success=true');
}
res.status(200).send({ ok: true, success: 'Email verified' });
}, function (err) {
if (redirectURL) {
let query = '?error=' + encodeURIComponent(err.error);
if (err.message) {
query += '&message=' + encodeURIComponent(err.message);
}
return res.status(201).redirect(redirectURL + query);
}
return next(err);
});
});
if (!disabled.includes('validate-username'))
router.get('/validate-username/:username', function (req, res, next) {
if (!req.params.username) {
return next({ error: 'Username required', status: 400 });
}
user.validateUsername(req.params.username).then(function (err_msg) {
if (!err_msg) {
res.status(200).json({ ok: true });
}
else {
res.status(409).json({ error: err_msg });
}
}, function (err) {
return next(err);
});
});
if (!disabled.includes('validate-email'))
router.get('/validate-email/:email', function (req, res, next) {
if (!req.params.email) {
return next({ error: 'Email required', status: 400 });
}
user.validateEmail(req.params.email).then(function (err) {
if (!err) {
res.status(200).json({ ok: true });
}
else if (err === user_1.ValidErr.emailInvalid) {
res.status(400).json({ error: user_1.ValidErr.emailInvalid });
}
else {
res.status(409).json({ error: 'Email already in use' });
}
}, function (err) {
return next(err);
});
});
if (!disabled.includes('request-deletion'))
router.post('/request-deletion', passport.authenticate('bearer', { session: false }), (req, res, next) => {
loginLocal(req, res, next);
}, (req, res, next) => {
if (req.body.reason && typeof req.body.reason !== 'string') {
return res.sendStatus(400);
}
res.status(200).json({
ok: true,
success: 'deletion requested'
});
user.removeUser(req.user._id, true, req.body.reason).catch(err => {
console.warn('request-deletion: failed for ', req.user._id, err);
});
});
if (!disabled.includes('change-email'))
router.post('/change-email', passport.authenticate('bearer', { session: false }), function (req, res, next) {
if (config.local.requirePasswordOnEmailChange) {
loginLocal(req, res, next);
}
else {
next(req);
}
}, function (req, res, next) {
const login = config.local.requirePasswordOnEmailChange
? req.user.key
: req.user._id;
user.changeEmail(login, req.body.newEmail, req).then(function () {
res
.status(200)
.json({ ok: true, success: 'Email change requested' });
}, function (err) {
return next(err);
});
});
if (!disabled.includes('session'))
router.get('/session', passport.authenticate('bearer', { session: false }), function (req, res) {
const user = req.user;
user.user_id = user._id; // todo: should rename/make clear what's used
delete user._id;
delete user.key;
res.status(200).json(user);
});
if (!disabled.includes('consents') && config.local.consents) {
router.get('/consents', passport.authenticate('bearer', { session: false }), (req, res, next) => {
user
.getCurrentConsents(req.user._id)
.then(consents => res.status(200).json(consents))
.catch(err => next(err));
});
router.post('/consents', passport.authenticate('bearer', { session: false }), (req, res, next) => {
user
.updateConsents(req.user._id, req.body)
.then(ret => res.status(200).json(ret))
.catch(err => next(err));
});
}
/**
* If the error is expected, it's sent as response. Otherwise, a generic
* server error without detailed information is returned when in production.
*/
router.use(function (err, req, res, next) {
const isExpected = (0, util_1.isUserFacingError)(err);
if (!isExpected) {
const errLog = typeof err === 'string' ? err : err.reason ? err.reason : err.message;
console.error(errLog);
if (err.stack) {
console.error(err.stack);
}
}
if (config.security?.forwardErrors === true) {
next(err);
}
else {
if (env !== 'development') {
isExpected
? res.status(err.status).json(err)
: res
.status(500)
.json({ status: 500, error: 'Internal Server Error' });
}
else {
res.status(err.status || 500).json(err);
}
}
});
}