From 56f47830758ff8e92abcfcc5f35d474ab12fe215 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Oct 2022 15:37:15 +0200 Subject: Support two factor authentication in backend --- server/middlewares/validators/shared/index.ts | 1 + server/middlewares/validators/shared/users.ts | 62 +++++++++++++++++++ server/middlewares/validators/two-factor.ts | 81 +++++++++++++++++++++++++ server/middlewares/validators/users.ts | 87 +++++++++------------------ 4 files changed, 174 insertions(+), 57 deletions(-) create mode 100644 server/middlewares/validators/shared/users.ts create mode 100644 server/middlewares/validators/two-factor.ts (limited to 'server/middlewares') diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts index bbd03b248..de98cd442 100644 --- a/server/middlewares/validators/shared/index.ts +++ b/server/middlewares/validators/shared/index.ts @@ -1,5 +1,6 @@ export * from './abuses' export * from './accounts' +export * from './users' export * from './utils' export * from './video-blacklists' export * from './video-captions' diff --git a/server/middlewares/validators/shared/users.ts b/server/middlewares/validators/shared/users.ts new file mode 100644 index 000000000..fbaa7db0e --- /dev/null +++ b/server/middlewares/validators/shared/users.ts @@ -0,0 +1,62 @@ +import express from 'express' +import { ActorModel } from '@server/models/actor/actor' +import { UserModel } from '@server/models/user/user' +import { MUserDefault } from '@server/types/models' +import { HttpStatusCode } from '@shared/models' + +function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { + const id = parseInt(idArg + '', 10) + return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) +} + +function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { + return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) +} + +async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { + const user = await UserModel.loadByUsernameOrEmail(username, email) + + if (user) { + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'User with this username or email already exists.' + }) + return false + } + + const actor = await ActorModel.loadLocalByName(username) + if (actor) { + res.fail({ + status: HttpStatusCode.CONFLICT_409, + message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' + }) + return false + } + + return true +} + +async function checkUserExist (finder: () => Promise, res: express.Response, abortResponse = true) { + const user = await finder() + + if (!user) { + if (abortResponse === true) { + res.fail({ + status: HttpStatusCode.NOT_FOUND_404, + message: 'User not found' + }) + } + + return false + } + + res.locals.user = user + return true +} + +export { + checkUserIdExist, + checkUserEmailExist, + checkUserNameOrEmailDoesNotAlreadyExist, + checkUserExist +} diff --git a/server/middlewares/validators/two-factor.ts b/server/middlewares/validators/two-factor.ts new file mode 100644 index 000000000..106b579b5 --- /dev/null +++ b/server/middlewares/validators/two-factor.ts @@ -0,0 +1,81 @@ +import express from 'express' +import { body, param } from 'express-validator' +import { HttpStatusCode, UserRight } from '@shared/models' +import { exists, isIdValid } from '../../helpers/custom-validators/misc' +import { areValidationErrors, checkUserIdExist } from './shared' + +const requestOrConfirmTwoFactorValidator = [ + param('id').custom(isIdValid), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + + if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return + + if (res.locals.user.otpSecret) { + return res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: `Two factor is already enabled.` + }) + } + + return next() + } +] + +const confirmTwoFactorValidator = [ + body('requestToken').custom(exists), + body('otpToken').custom(exists), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + + return next() + } +] + +const disableTwoFactorValidator = [ + param('id').custom(isIdValid), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + + if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return + + if (!res.locals.user.otpSecret) { + return res.fail({ + status: HttpStatusCode.BAD_REQUEST_400, + message: `Two factor is already disabled.` + }) + } + + return next() + } +] + +// --------------------------------------------------------------------------- + +export { + requestOrConfirmTwoFactorValidator, + confirmTwoFactorValidator, + disableTwoFactorValidator +} + +// --------------------------------------------------------------------------- + +async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) { + const authUser = res.locals.oauth.token.user + + if (!await checkUserIdExist(userId, res)) return + + if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) { + res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: `User ${authUser.username} does not have right to change two factor setting of this user.` + }) + + return false + } + + return true +} diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index eb693318f..046029547 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -1,9 +1,8 @@ import express from 'express' import { body, param, query } from 'express-validator' import { Hooks } from '@server/lib/plugins/hooks' -import { MUserDefault } from '@server/types/models' import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' -import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' +import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isUserAdminFlagsValid, @@ -30,8 +29,15 @@ import { isThemeRegistered } from '../../lib/plugins/theme-utils' import { Redis } from '../../lib/redis' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' import { ActorModel } from '../../models/actor/actor' -import { UserModel } from '../../models/user/user' -import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared' +import { + areValidationErrors, + checkUserEmailExist, + checkUserIdExist, + checkUserNameOrEmailDoesNotAlreadyExist, + doesVideoChannelIdExist, + doesVideoExist, + isValidVideoIdParam +} from './shared' const usersListValidator = [ query('blocked') @@ -435,7 +441,7 @@ const usersResetPasswordValidator = [ if (!await checkUserIdExist(req.params.id, res)) return const user = res.locals.user - const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) + const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id) if (redisVerificationString !== req.body.verificationString) { return res.fail({ @@ -500,6 +506,24 @@ const usersVerifyEmailValidator = [ } ] +const usersCheckCurrentPassword = [ + body('currentPassword').custom(exists), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (areValidationErrors(req, res)) return + + const user = res.locals.oauth.token.User + if (await user.isPasswordMatch(req.body.currentPassword) !== true) { + return res.fail({ + status: HttpStatusCode.FORBIDDEN_403, + message: 'currentPassword is invalid.' + }) + } + + return next() + } +] + const userAutocompleteValidator = [ param('search') .isString() @@ -567,6 +591,7 @@ export { usersUpdateValidator, usersUpdateMeValidator, usersVideoRatingValidator, + usersCheckCurrentPassword, ensureUserRegistrationAllowed, ensureUserRegistrationAllowedForIP, usersGetValidator, @@ -580,55 +605,3 @@ export { ensureCanModerateUser, ensureCanManageChannelOrAccount } - -// --------------------------------------------------------------------------- - -function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { - const id = parseInt(idArg + '', 10) - return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) -} - -function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { - return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) -} - -async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { - const user = await UserModel.loadByUsernameOrEmail(username, email) - - if (user) { - res.fail({ - status: HttpStatusCode.CONFLICT_409, - message: 'User with this username or email already exists.' - }) - return false - } - - const actor = await ActorModel.loadLocalByName(username) - if (actor) { - res.fail({ - status: HttpStatusCode.CONFLICT_409, - message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' - }) - return false - } - - return true -} - -async function checkUserExist (finder: () => Promise, res: express.Response, abortResponse = true) { - const user = await finder() - - if (!user) { - if (abortResponse === true) { - res.fail({ - status: HttpStatusCode.NOT_FOUND_404, - message: 'User not found' - }) - } - - return false - } - - res.locals.user = user - return true -} -- cgit v1.2.3