UNPKG

@perfood/couch-auth

Version:

Easy and secure authentication for CouchDB/Cloudant. Based on SuperLogin, updated and rewritten in Typescript.

381 lines (380 loc) 16.4 kB
'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); 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: config.security.loginRateLimit ? config.security.loginRateLimit.delayMs || 500 : 0, 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]; }, onLimitReached: config.security.loginRateLimit?.onLimitReached || function () { }, 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: config.security.passwordResetRateLimit ? config.security.passwordResetRateLimit.delayMs || 500 : 0, 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]; }, onLimitReached: config.security.passwordResetRateLimit?.onLimitReached || function () { }, 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); } } }); } exports.default = default_1;