Add enable/disable service methods

This commit is contained in:
rijkvanzanten
2020-08-15 20:34:41 -06:00
parent d918b48197
commit a9cc4755ef
4 changed files with 57 additions and 31 deletions

View File

@@ -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,
}
);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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 });
}
}