mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add enable/disable service methods
This commit is contained in:
@@ -17,6 +17,7 @@ const loginSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().required(),
|
||||
mode: Joi.string().valid('cookie', 'json'),
|
||||
otp: Joi.string()
|
||||
});
|
||||
|
||||
router.post(
|
||||
@@ -35,7 +36,7 @@ router.post(
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const { email, password } = req.body;
|
||||
const { email, password, otp } = req.body;
|
||||
|
||||
const mode = req.body.mode || 'json';
|
||||
|
||||
@@ -48,6 +49,7 @@ router.post(
|
||||
userAgent,
|
||||
email,
|
||||
password,
|
||||
otp,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { InvalidPayloadException, InvalidCredentialsException } from '../excepti
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import UsersService from '../services/users';
|
||||
import MetaService from '../services/meta';
|
||||
import AuthService from '../services/authentication';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -163,4 +164,27 @@ router.post('/me/tfa/enable/', asyncHandler(async (req, res) => {
|
||||
return res.json({ data: { otpauth_url: url }});
|
||||
}));
|
||||
|
||||
router.post('/me/tfa/disable', asyncHandler(async (req, res) => {
|
||||
if (!req.accountability?.user) {
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
if (!req.body.otp) {
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const authService = new AuthService({ accountability: req.accountability });
|
||||
|
||||
const otpValid = await authService.verifyOTP(req.accountability.user, req.body.otp);
|
||||
|
||||
if (otpValid === false) {
|
||||
throw new InvalidPayloadException(`"otp" is invalid`);
|
||||
}
|
||||
|
||||
await service.disableTFA(req.accountability.user);
|
||||
|
||||
return res.status(200).end();
|
||||
}));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,9 +8,7 @@ import { Session, Accountability, AbstractServiceOptions, Action } from '../type
|
||||
import Knex from 'knex';
|
||||
import ActivityService from '../services/activity';
|
||||
import env from '../env';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import url from 'url';
|
||||
import base32 from 'hi-base32';
|
||||
import { authenticator } from 'otplib';
|
||||
|
||||
type AuthenticateOptions = {
|
||||
email: string;
|
||||
@@ -56,6 +54,14 @@ export default class AuthenticationService {
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
if (user.tfa_secret && otp) {
|
||||
const otpValid = await this.verifyOTP(user.id, otp);
|
||||
|
||||
if (otpValid === false) {
|
||||
throw new InvalidPayloadException(`"otp" is invalid`);
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: user.id,
|
||||
};
|
||||
@@ -135,34 +141,24 @@ export default class AuthenticationService {
|
||||
}
|
||||
|
||||
generateTFASecret() {
|
||||
const set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()<>?/[]{},.:;';
|
||||
const nanoid = customAlphabet(set, 32);
|
||||
return nanoid();
|
||||
const secret = authenticator.generateSecret();
|
||||
return secret;
|
||||
}
|
||||
|
||||
async generateOTPAuthURL(secret: string) {
|
||||
const settings = await this.knex.select('project_name').from('directus_settings').first();
|
||||
const label = settings?.project_name || 'Directus';
|
||||
|
||||
const urlSecret = base32.encode(secret);
|
||||
|
||||
const query = {
|
||||
secret: urlSecret,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
};
|
||||
|
||||
return url.format({
|
||||
protocol: 'otpauth',
|
||||
slashes: true,
|
||||
hostname: 'totp',
|
||||
pathname: encodeURIComponent(label),
|
||||
query
|
||||
});
|
||||
async generateOTPAuthURL(pk: string, secret: string) {
|
||||
const user = await this.knex.select('first_name', 'last_name').from('directus_users').where({ id: pk }).first();
|
||||
const name = `${user.first_name} ${user.last_name}`;
|
||||
return authenticator.keyuri(name, 'Directus', secret);
|
||||
}
|
||||
|
||||
verifyOTP(secret: string, token: string): boolean {
|
||||
return true;
|
||||
async verifyOTP(pk: string, otp: string): Promise<boolean> {
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
|
||||
if (!user.tfa_secret) {
|
||||
throw new InvalidPayloadException(`User "${pk}" doesn't have TFA enabled.`);
|
||||
}
|
||||
|
||||
const secret = user.tfa_secret;
|
||||
return authenticator.check(otp, secret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default class UsersService extends ItemsService {
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
|
||||
if (user?.tfa_secret !== null) {
|
||||
throw new Error('TFA Secret is already set for this user');
|
||||
throw new InvalidPayloadException('TFA Secret is already set for this user');
|
||||
}
|
||||
|
||||
const authService = new AuthService();
|
||||
@@ -64,6 +64,10 @@ export default class UsersService extends ItemsService {
|
||||
|
||||
await this.knex('directus_users').update({ tfa_secret: secret }).where({ id: pk });
|
||||
|
||||
return await authService.generateOTPAuthURL(secret);
|
||||
return await authService.generateOTPAuthURL(pk, secret);
|
||||
}
|
||||
|
||||
async disableTFA(pk: string) {
|
||||
await this.knex('directus_users').update({ tfa_secret: null }).where({ id: pk });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user