tethys.backend/app/Services/TwoFactorAuthProvider.ts
Arno Kaimbacher f828ca4491
All checks were successful
CI Pipeline / japa-tests (push) Successful in 1m2s
- added 2fa authentication during login. see resources/js/Pages/Auth/login.vue
- added validate() method inside app/Srvices/TwoFactorProvider.ts
- added twoFactorChallenge() method inside app/Controllers/Http/Auth/AuthController.ts for logging in via 2fa-code
2024-02-16 15:32:47 +01:00

127 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
* Were providing the name of the app and the users 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 were 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<boolean> {
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<boolean> {
const isValid = verifyToken(user.twoFactorSecret as string, token, 1);
if (isValid) {
return true;
}
return false;
}
}
export default new TwoFactorAuthProvider();