diff options
author | Chocobozzz <me@florianbigard.com> | 2022-10-05 15:37:15 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-10-07 10:51:16 +0200 |
commit | 56f47830758ff8e92abcfcc5f35d474ab12fe215 (patch) | |
tree | 854e57ec1b800d6ad740c8e42bee00cbd21e1724 /server/middlewares/validators | |
parent | 7dd7ff4cebc290b09fe00d82046bb58e4e8a800d (diff) | |
download | PeerTube-56f47830758ff8e92abcfcc5f35d474ab12fe215.tar.gz PeerTube-56f47830758ff8e92abcfcc5f35d474ab12fe215.tar.zst PeerTube-56f47830758ff8e92abcfcc5f35d474ab12fe215.zip |
Support two factor authentication in backend
Diffstat (limited to 'server/middlewares/validators')
-rw-r--r-- | server/middlewares/validators/shared/index.ts | 1 | ||||
-rw-r--r-- | server/middlewares/validators/shared/users.ts | 62 | ||||
-rw-r--r-- | server/middlewares/validators/two-factor.ts | 81 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 87 |
4 files changed, 174 insertions, 57 deletions
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 @@ | |||
1 | export * from './abuses' | 1 | export * from './abuses' |
2 | export * from './accounts' | 2 | export * from './accounts' |
3 | export * from './users' | ||
3 | export * from './utils' | 4 | export * from './utils' |
4 | export * from './video-blacklists' | 5 | export * from './video-blacklists' |
5 | export * from './video-captions' | 6 | 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 @@ | |||
1 | import express from 'express' | ||
2 | import { ActorModel } from '@server/models/actor/actor' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { HttpStatusCode } from '@shared/models' | ||
6 | |||
7 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { | ||
8 | const id = parseInt(idArg + '', 10) | ||
9 | return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) | ||
10 | } | ||
11 | |||
12 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
13 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | ||
14 | } | ||
15 | |||
16 | async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { | ||
17 | const user = await UserModel.loadByUsernameOrEmail(username, email) | ||
18 | |||
19 | if (user) { | ||
20 | res.fail({ | ||
21 | status: HttpStatusCode.CONFLICT_409, | ||
22 | message: 'User with this username or email already exists.' | ||
23 | }) | ||
24 | return false | ||
25 | } | ||
26 | |||
27 | const actor = await ActorModel.loadLocalByName(username) | ||
28 | if (actor) { | ||
29 | res.fail({ | ||
30 | status: HttpStatusCode.CONFLICT_409, | ||
31 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' | ||
32 | }) | ||
33 | return false | ||
34 | } | ||
35 | |||
36 | return true | ||
37 | } | ||
38 | |||
39 | async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) { | ||
40 | const user = await finder() | ||
41 | |||
42 | if (!user) { | ||
43 | if (abortResponse === true) { | ||
44 | res.fail({ | ||
45 | status: HttpStatusCode.NOT_FOUND_404, | ||
46 | message: 'User not found' | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | return false | ||
51 | } | ||
52 | |||
53 | res.locals.user = user | ||
54 | return true | ||
55 | } | ||
56 | |||
57 | export { | ||
58 | checkUserIdExist, | ||
59 | checkUserEmailExist, | ||
60 | checkUserNameOrEmailDoesNotAlreadyExist, | ||
61 | checkUserExist | ||
62 | } | ||
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 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
4 | import { exists, isIdValid } from '../../helpers/custom-validators/misc' | ||
5 | import { areValidationErrors, checkUserIdExist } from './shared' | ||
6 | |||
7 | const requestOrConfirmTwoFactorValidator = [ | ||
8 | param('id').custom(isIdValid), | ||
9 | |||
10 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
11 | if (areValidationErrors(req, res)) return | ||
12 | |||
13 | if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return | ||
14 | |||
15 | if (res.locals.user.otpSecret) { | ||
16 | return res.fail({ | ||
17 | status: HttpStatusCode.BAD_REQUEST_400, | ||
18 | message: `Two factor is already enabled.` | ||
19 | }) | ||
20 | } | ||
21 | |||
22 | return next() | ||
23 | } | ||
24 | ] | ||
25 | |||
26 | const confirmTwoFactorValidator = [ | ||
27 | body('requestToken').custom(exists), | ||
28 | body('otpToken').custom(exists), | ||
29 | |||
30 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
31 | if (areValidationErrors(req, res)) return | ||
32 | |||
33 | return next() | ||
34 | } | ||
35 | ] | ||
36 | |||
37 | const disableTwoFactorValidator = [ | ||
38 | param('id').custom(isIdValid), | ||
39 | |||
40 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
41 | if (areValidationErrors(req, res)) return | ||
42 | |||
43 | if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return | ||
44 | |||
45 | if (!res.locals.user.otpSecret) { | ||
46 | return res.fail({ | ||
47 | status: HttpStatusCode.BAD_REQUEST_400, | ||
48 | message: `Two factor is already disabled.` | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | return next() | ||
53 | } | ||
54 | ] | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | export { | ||
59 | requestOrConfirmTwoFactorValidator, | ||
60 | confirmTwoFactorValidator, | ||
61 | disableTwoFactorValidator | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) { | ||
67 | const authUser = res.locals.oauth.token.user | ||
68 | |||
69 | if (!await checkUserIdExist(userId, res)) return | ||
70 | |||
71 | if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) { | ||
72 | res.fail({ | ||
73 | status: HttpStatusCode.FORBIDDEN_403, | ||
74 | message: `User ${authUser.username} does not have right to change two factor setting of this user.` | ||
75 | }) | ||
76 | |||
77 | return false | ||
78 | } | ||
79 | |||
80 | return true | ||
81 | } | ||
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' | 4 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' |
6 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 5 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 6 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
8 | import { | 7 | import { |
9 | isUserAdminFlagsValid, | 8 | isUserAdminFlagsValid, |
@@ -30,8 +29,15 @@ import { isThemeRegistered } from '../../lib/plugins/theme-utils' | |||
30 | import { Redis } from '../../lib/redis' | 29 | import { Redis } from '../../lib/redis' |
31 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' | 30 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' |
32 | import { ActorModel } from '../../models/actor/actor' | 31 | import { ActorModel } from '../../models/actor/actor' |
33 | import { UserModel } from '../../models/user/user' | 32 | import { |
34 | import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared' | 33 | areValidationErrors, |
34 | checkUserEmailExist, | ||
35 | checkUserIdExist, | ||
36 | checkUserNameOrEmailDoesNotAlreadyExist, | ||
37 | doesVideoChannelIdExist, | ||
38 | doesVideoExist, | ||
39 | isValidVideoIdParam | ||
40 | } from './shared' | ||
35 | 41 | ||
36 | const usersListValidator = [ | 42 | const usersListValidator = [ |
37 | query('blocked') | 43 | query('blocked') |
@@ -435,7 +441,7 @@ const usersResetPasswordValidator = [ | |||
435 | if (!await checkUserIdExist(req.params.id, res)) return | 441 | if (!await checkUserIdExist(req.params.id, res)) return |
436 | 442 | ||
437 | const user = res.locals.user | 443 | const user = res.locals.user |
438 | const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) | 444 | const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id) |
439 | 445 | ||
440 | if (redisVerificationString !== req.body.verificationString) { | 446 | if (redisVerificationString !== req.body.verificationString) { |
441 | return res.fail({ | 447 | return res.fail({ |
@@ -500,6 +506,24 @@ const usersVerifyEmailValidator = [ | |||
500 | } | 506 | } |
501 | ] | 507 | ] |
502 | 508 | ||
509 | const usersCheckCurrentPassword = [ | ||
510 | body('currentPassword').custom(exists), | ||
511 | |||
512 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
513 | if (areValidationErrors(req, res)) return | ||
514 | |||
515 | const user = res.locals.oauth.token.User | ||
516 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { | ||
517 | return res.fail({ | ||
518 | status: HttpStatusCode.FORBIDDEN_403, | ||
519 | message: 'currentPassword is invalid.' | ||
520 | }) | ||
521 | } | ||
522 | |||
523 | return next() | ||
524 | } | ||
525 | ] | ||
526 | |||
503 | const userAutocompleteValidator = [ | 527 | const userAutocompleteValidator = [ |
504 | param('search') | 528 | param('search') |
505 | .isString() | 529 | .isString() |
@@ -567,6 +591,7 @@ export { | |||
567 | usersUpdateValidator, | 591 | usersUpdateValidator, |
568 | usersUpdateMeValidator, | 592 | usersUpdateMeValidator, |
569 | usersVideoRatingValidator, | 593 | usersVideoRatingValidator, |
594 | usersCheckCurrentPassword, | ||
570 | ensureUserRegistrationAllowed, | 595 | ensureUserRegistrationAllowed, |
571 | ensureUserRegistrationAllowedForIP, | 596 | ensureUserRegistrationAllowedForIP, |
572 | usersGetValidator, | 597 | usersGetValidator, |
@@ -580,55 +605,3 @@ export { | |||
580 | ensureCanModerateUser, | 605 | ensureCanModerateUser, |
581 | ensureCanManageChannelOrAccount | 606 | ensureCanManageChannelOrAccount |
582 | } | 607 | } |
583 | |||
584 | // --------------------------------------------------------------------------- | ||
585 | |||
586 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { | ||
587 | const id = parseInt(idArg + '', 10) | ||
588 | return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) | ||
589 | } | ||
590 | |||
591 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
592 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | ||
593 | } | ||
594 | |||
595 | async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { | ||
596 | const user = await UserModel.loadByUsernameOrEmail(username, email) | ||
597 | |||
598 | if (user) { | ||
599 | res.fail({ | ||
600 | status: HttpStatusCode.CONFLICT_409, | ||
601 | message: 'User with this username or email already exists.' | ||
602 | }) | ||
603 | return false | ||
604 | } | ||
605 | |||
606 | const actor = await ActorModel.loadLocalByName(username) | ||
607 | if (actor) { | ||
608 | res.fail({ | ||
609 | status: HttpStatusCode.CONFLICT_409, | ||
610 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' | ||
611 | }) | ||
612 | return false | ||
613 | } | ||
614 | |||
615 | return true | ||
616 | } | ||
617 | |||
618 | async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) { | ||
619 | const user = await finder() | ||
620 | |||
621 | if (!user) { | ||
622 | if (abortResponse === true) { | ||
623 | res.fail({ | ||
624 | status: HttpStatusCode.NOT_FOUND_404, | ||
625 | message: 'User not found' | ||
626 | }) | ||
627 | } | ||
628 | |||
629 | return false | ||
630 | } | ||
631 | |||
632 | res.locals.user = user | ||
633 | return true | ||
634 | } | ||