@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
66 lines (65 loc) • 3.1 kB
JavaScript
import { InvalidPayloadError } from '@directus/errors';
import { authenticator } from 'otplib';
import getDatabase from '../database/index.js';
import { ItemsService } from './items.js';
import { DEFAULT_AUTH_PROVIDER } from '../constants.js';
export class TFAService {
knex;
itemsService;
constructor(options) {
this.knex = options.knex || getDatabase();
this.itemsService = new ItemsService('directus_users', options);
}
async verifyOTP(key, otp, secret) {
if (secret) {
return authenticator.check(otp, secret);
}
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: key }).first();
if (!user?.tfa_secret) {
throw new InvalidPayloadError({ reason: `User "${key}" doesn't have TFA enabled` });
}
return authenticator.check(otp, user.tfa_secret);
}
async generateTFA(key, requiresPassword = true) {
const user = await this.knex
.select('email', 'tfa_secret', 'provider', 'external_identifier')
.from('directus_users')
.where({ id: key })
.first();
if (user?.tfa_secret !== null) {
throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
}
// Only require email for non-OAuth users
if (user?.provider === DEFAULT_AUTH_PROVIDER && !user?.email) {
throw new InvalidPayloadError({ reason: 'User must have a valid email to enable TFA' });
}
if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
}
const secret = authenticator.generateSecret();
const project = await this.knex.select('project_name').from('directus_settings').limit(1).first();
// For OAuth users without email, use external_identifier as fallback
const accountName = user.email || user.external_identifier || `user_${key}`;
return {
secret,
url: authenticator.keyuri(accountName, project?.project_name || 'Directus', secret),
};
}
async enableTFA(key, otp, secret) {
const user = await this.knex.select('tfa_secret', 'provider').from('directus_users').where({ id: key }).first();
const requiresPassword = user?.['provider'] === DEFAULT_AUTH_PROVIDER;
if (user?.tfa_secret !== null) {
throw new InvalidPayloadError({ reason: 'TFA Secret is already set for this user' });
}
if (!requiresPassword && user?.provider === DEFAULT_AUTH_PROVIDER) {
throw new InvalidPayloadError({ reason: 'This method is only available for OAuth users' });
}
if (!authenticator.check(otp, secret)) {
throw new InvalidPayloadError({ reason: `"otp" is invalid` });
}
await this.itemsService.updateOne(key, { tfa_secret: secret });
}
async disableTFA(key) {
await this.itemsService.updateOne(key, { tfa_secret: null });
}
}