mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add otp enable endpoint
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user