Add tfa enforce flow (#7805)

* add tfa enforce flow

* add 'tfa-secret' to recommended permissions

* fix if theme if user has dark mode

* oas: rename 'enable-2fa' to 'enable-tfa'

* Add required user fields

* Uniformize styling

* Fix direct and invalid routing

* Add required permission docs

* Fix typescript warnings

* Fix typescript warnings 2

* Allow auto theme

* Nest duplicate condition check

* Fix routing for users without role

* Follow page redirects

* Reduce the use of redirect query

* Improve error UX

* Allow admins to disable 2FA

* Improve autofocus UX

* Override update permission for 'tfa_secret' when role enforces TFA

* Remove permission requirements from docs

Co-authored-by: Jose Varela <joselcvarela@gmail.com>
Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
Co-authored-by: ian <licitdev@gmail.com>
This commit is contained in:
Nitwel
2022-06-15 19:27:59 +02:00
committed by GitHub
parent 3b2245918d
commit 37762358fc
13 changed files with 479 additions and 105 deletions

View File

@@ -4,9 +4,10 @@ import { InvalidCredentialsException, ForbiddenException, InvalidPayloadExceptio
import { respond } from '../middleware/respond';
import useCollection from '../middleware/use-collection';
import { validateBatch } from '../middleware/validate-batch';
import { AuthenticationService, MetaService, UsersService, TFAService } from '../services';
import { AuthenticationService, MetaService, UsersService, RolesService, TFAService } from '../services';
import { PrimaryKey } from '../types';
import asyncHandler from '../utils/async-handler';
import { Role } from '@directus/shared/types';
const router = express.Router();
@@ -354,6 +355,37 @@ router.post(
throw new InvalidPayloadException(`"otp" is required`);
}
// Override permissions only when enforce TFA is enabled in role
if (req.accountability.role) {
const rolesService = new RolesService({
schema: req.schema,
});
const role = (await rolesService.readOne(req.accountability.role)) as Role;
if (role && role.enforce_tfa) {
const existingPermission = await req.accountability.permissions?.find(
(p) => p.collection === 'directus_users' && p.action === 'update'
);
if (existingPermission) {
existingPermission.fields = ['tfa_secret'];
existingPermission.permissions = { id: { _eq: req.accountability.user } };
existingPermission.presets = null;
existingPermission.validation = null;
} else {
(req.accountability.permissions || (req.accountability.permissions = [])).push({
action: 'update',
collection: 'directus_users',
fields: ['tfa_secret'],
permissions: { id: { _eq: req.accountability.user } },
presets: null,
role: req.accountability.role,
validation: null,
});
}
}
}
const service = new TFAService({
accountability: req.accountability,
schema: req.schema,
@@ -377,6 +409,37 @@ router.post(
throw new InvalidPayloadException(`"otp" is required`);
}
// Override permissions only when enforce TFA is enabled in role
if (req.accountability.role) {
const rolesService = new RolesService({
schema: req.schema,
});
const role = (await rolesService.readOne(req.accountability.role)) as Role;
if (role && role.enforce_tfa) {
const existingPermission = await req.accountability.permissions?.find(
(p) => p.collection === 'directus_users' && p.action === 'update'
);
if (existingPermission) {
existingPermission.fields = ['tfa_secret'];
existingPermission.permissions = { id: { _eq: req.accountability.user } };
existingPermission.presets = null;
existingPermission.validation = null;
} else {
(req.accountability.permissions || (req.accountability.permissions = [])).push({
action: 'update',
collection: 'directus_users',
fields: ['tfa_secret'],
permissions: { id: { _eq: req.accountability.user } },
presets: null,
role: req.accountability.role,
validation: null,
});
}
}
}
const service = new TFAService({
accountability: req.accountability,
schema: req.schema,
@@ -394,4 +457,26 @@ router.post(
respond
);
router.post(
'/:pk/tfa/disable',
asyncHandler(async (req, _res, next) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
if (!req.accountability.admin || !req.params.pk) {
throw new ForbiddenException();
}
const service = new TFAService({
accountability: req.accountability,
schema: req.schema,
});
await service.disableTFA(req.params.pk);
return next();
}),
respond
);
export default router;