import Config from '@ioc:Adonis/Core/Config'; import User from 'App/Models/User'; import { generateSecret, verifyToken } from 'node-2fa/dist/index'; // import cryptoRandomString from 'crypto-random-string'; import QRCode from 'qrcode'; import crypto from 'crypto'; import { TotpState } from 'Contracts/enums'; // npm install node-2fa --save // npm install crypto-random-string --save // import { cryptoRandomStringAsync } from 'crypto-random-string/index'; // npm install qrcode --save // npm i --save-dev @types/qrcode class TwoFactorAuthProvider { private issuer = Config.get('twoFactorAuthConfig.app.name') || 'TethysCloud'; /** * generateSecret will generate a user-specific 32-character secret. * We’re providing the name of the app and the user’s email as parameters for the function. * This secret key will be used to verify whether the token provided by the user during authentication is valid or not. * * Return the default global focus trap stack * * @param {User} user user for the secrect * @return {string} */ public generateSecret(user: User) { const secret = generateSecret({ name: this.issuer, account: user.email, }); return secret.secret; } /** * We also generated recovery codes which can be used in case we’re unable to retrieve tokens from 2FA applications. * We assign the user a list of recovery codes and each code can be used only once during the authentication process. * The recovery codes are random strings generated using the cryptoRandomString library. * * Return recovery codes * @return {string[]} */ public generateRecoveryCodes() { const recoveryCodeLimit: number = 8; const codes: string[] = []; for (let i = 0; i < recoveryCodeLimit; i++) { const recoveryCode: string = `${this.secureRandomString()}-${this.secureRandomString()}`; codes.push(recoveryCode); } return codes; } private secureRandomString() { // return await cryptoRandomString.async({ length: 10, type: 'hex' }); return this.generateRandomString(10, 'hex'); } private generateRandomString(length: number, type: 'hex' | 'base64' | 'numeric' = 'hex'): string { const byteLength = Math.ceil(length * 0.5); // For hex encoding, each byte generates 2 characters const randomBytes = crypto.randomBytes(byteLength); switch (type) { case 'hex': return randomBytes.toString('hex').slice(0, length); case 'base64': return randomBytes.toString('base64').slice(0, length); case 'numeric': return randomBytes .toString('hex') .replace(/[a-fA-F]/g, '') // Remove non-numeric characters .slice(0, length); default: throw new Error('Invalid type specified'); } } // public async generateQrCode(user: User) : Promise<{svg: string; url: string; secret: string; }> { // const issuer = encodeURIComponent(this.issuer); // 'TethysCloud' // // const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at' // const label = `${this.issuer}:${user.email}`; // const algorithm = encodeURIComponent("SHA256"); // const query = `?secret=${user.twoFactorSecret}&issuer=${issuer}&algorithm=${algorithm}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud' // const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud' // const svg = await QRCode.toDataURL(url); // const secret = user.twoFactorSecret as string; // return { svg, url, secret }; // } public async generateQrCode(user: User, twoFactorSecret?: string): Promise<{ svg: string; url: string; secret: string }> { const issuer = encodeURIComponent(this.issuer); // 'TethysCloud' // const userName = encodeURIComponent(user.email); // 'rrqx9472%40tethys.at' const label = `${this.issuer}:${user.email}`; // const algorithm = encodeURIComponent('SHA256'); const secret = twoFactorSecret ? twoFactorSecret : (user.twoFactorSecret as string); const query = `?secret=${secret}&issuer=${issuer}&digits=6`; // '?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud' const url = `otpauth://totp/${label}${query}`; // 'otpauth://totp/rrqx9472%40tethys.at?secret=FEYCLOSO627CB7SMLX6QQ7BP75L7SJ54&issuer=TethysCloud' const svg = await QRCode.toDataURL(url); return { svg, url, secret }; } public async enable(user: User, token: string): Promise { const isValid = verifyToken(user.twoFactorSecret as string, token, 1); if (!isValid) { return false; } user.state = TotpState.STATE_ENABLED; if (await user.save()) { return true; } return false; } public async validate(user: User, token: string): Promise { const isValid = verifyToken(user.twoFactorSecret as string, token, 1); if (isValid) { return true; } return false; } } export default new TwoFactorAuthProvider();