Add otp enable endpoint

This commit is contained in:
rijkvanzanten
2020-08-15 18:11:11 -06:00
parent a90a5ca99d
commit d9ae271612
3 changed files with 71 additions and 6 deletions

View File

@@ -152,4 +152,15 @@ router.post(
})
);
router.post('/me/tfa/enable/', asyncHandler(async (req, res) => {
if (!req.accountability?.user) {
throw new InvalidCredentialsException();
}
const service = new UsersService({ accountability: req.accountability });
const url = await service.enableTFA(req.accountability.user);
return res.json({ data: { otpauth_url: url }});
}));
export default router;

View File

@@ -3,17 +3,21 @@ import jwt from 'jsonwebtoken';
import argon2 from 'argon2';
import { nanoid } from 'nanoid';
import ms from 'ms';
import { InvalidCredentialsException } from '../exceptions';
import { InvalidCredentialsException, InvalidPayloadException } from '../exceptions';
import { Session, Accountability, AbstractServiceOptions, Action } from '../types';
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';
type AuthenticateOptions = {
email: string;
password?: string;
ip?: string | null;
userAgent?: string | null;
otp?: string;
};
export default class AuthenticationService {
@@ -33,16 +37,14 @@ export default class AuthenticationService {
* Password is optional to allow usage of this function within the SSO flow and extensions. Make sure
* to handle password existence checks elsewhere
*/
async authenticate({ email, password, ip, userAgent }: AuthenticateOptions) {
async authenticate({ email, password, ip, userAgent, otp }: AuthenticateOptions) {
const user = await database
.select('id', 'password', 'role')
.select('id', 'password', 'role', 'tfa_secret', 'status')
.from('directus_users')
.where({ email })
.first();
/** @todo check for status */
if (!user) {
if (!user || user.status !== 'active') {
throw new InvalidCredentialsException();
}
@@ -50,6 +52,10 @@ export default class AuthenticationService {
throw new InvalidCredentialsException();
}
if (user.tfa_secret && !otp) {
throw new InvalidPayloadException(`"otp" is required`);
}
const payload = {
id: user.id,
};
@@ -127,4 +133,36 @@ export default class AuthenticationService {
async logout(refreshToken: string) {
await this.knex.delete().from('directus_sessions').where({ token: refreshToken });
}
generateTFASecret() {
const set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()<>?/[]{},.:;';
const nanoid = customAlphabet(set, 32);
return nanoid();
}
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
});
}
verifyOTP(secret: string, token: string): boolean {
return true;
}
}

View File

@@ -1,3 +1,4 @@
import AuthService from './authentication';
import ItemsService from './items';
import jwt from 'jsonwebtoken';
import { sendInviteMail } from '../mail';
@@ -50,4 +51,19 @@ export default class UsersService extends ItemsService {
.update({ password: passwordHashed, status: 'active' })
.where({ id: user.id });
}
async enableTFA(pk: string) {
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');
}
const authService = new AuthService();
const secret = authService.generateTFASecret();
await this.knex('directus_users').update({ tfa_secret: secret }).where({ id: pk });
return await authService.generateOTPAuthURL(secret);
}
}