- added backup codes for 2 factor authentication
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s

- npm updates
- coverage validation: elevation ust be positive, depth must be negative
- vinejs-provider.js: get enabled extensions from database, not via validOptions.extnames
- vue components for backup codes: e.g.: PersonalSettings.vue
- validate spaital coverage in leaflet map: draw.component.vue, map.component.vue
- add backup code authentication into Login.vue
- preset to use no preferred reviewer: Release.vue
- 2 new vinejs validation rules: file_scan.ts and file-length.ts
This commit is contained in:
Kaimbacher 2024-07-08 13:52:20 +02:00
parent ac473b1e72
commit 005df2e454
32 changed files with 1416 additions and 526 deletions

View file

@ -1,5 +1,6 @@
// import Config from '@ioc:Adonis/Core/Config';
import config from '@adonisjs/core/services/config'
// import config from '@adonisjs/core/services/config'
import env from '#start/env';
import User from '#models/user';
import { generateSecret, verifyToken } from 'node-2fa/dist/index.js';
// import cryptoRandomString from 'crypto-random-string';
@ -14,7 +15,7 @@ import { TotpState } from '#contracts/enums';
// npm i --save-dev @types/qrcode
class TwoFactorAuthProvider {
private issuer: string = config.get('twoFactorAuthConfig.app.name') || 'TethysCloud';
private issuer: string = env.get('APP_NAME') || 'TethysCloud';
/**
* generateSecret will generate a user-specific 32-character secret.
@ -41,7 +42,7 @@ class TwoFactorAuthProvider {
* Return recovery codes
* @return {string[]}
*/
public generateRecoveryCodes() {
public generateRecoveryCodes(): string[] {
const recoveryCodeLimit: number = 8;
const codes: string[] = [];
for (let i = 0; i < recoveryCodeLimit; i++) {

View file

@ -0,0 +1,136 @@
import User from '#models/user';
import BackupCode from '#models/backup_code';
import hash from '@adonisjs/core/services/hash';
export interface ISecureRandom {
CHAR_UPPER: string;
CHAR_LOWER: string;
CHAR_DIGITS: string;
CHAR_SYMBOLS: string;
CHAR_ALPHANUMERIC: string;
CHAR_HUMAN_READABLE: string;
/**
* Generate a random string of specified length.
* @param int $length The length of the generated string
* @param string $characters An optional list of characters to use if no character list is
* specified all valid base64 characters are used.
* @return string
* @since 8.0.0
*/
generate(length: number, characters?: string): string;
}
export class SecureRandom implements ISecureRandom {
CHAR_UPPER: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
CHAR_LOWER: string = 'abcdefghijklmnopqrstuvwxyz';
CHAR_DIGITS: string = '0123456789';
CHAR_SYMBOLS: string = '!"#$%&\\\'()*+,-./:;<=>?@[]^_`{|}~';
CHAR_ALPHANUMERIC: string = this.CHAR_UPPER + this.CHAR_LOWER + this.CHAR_DIGITS;
CHAR_HUMAN_READABLE: string = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789';
public generate(length: number, characters: string = this.CHAR_ALPHANUMERIC): string {
if (length <= 0) {
throw new Error('Invalid length specified: ' + length + ' must be bigger than 0');
}
const maxCharIndex: number = characters.length - 1;
let randomString: string = '';
while (length > 0) {
const randomNumber: number = Math.floor(Math.random() * (maxCharIndex + 1));
randomString += characters[randomNumber];
length--;
}
return randomString;
}
}
class BackupCodeStorage {
private static CODE_LENGTH: number = 16;
// private mapper: BackupCodeMapper;
// private hasher: IHasher;
private random: ISecureRandom;
// private eventDispatcher: IEventDispatcher;
// constructor(mapper: BackupCodeMapper, random: ISecureRandom, hasher: IHasher, eventDispatcher: IEventDispatcher) {
// this.mapper = mapper;
// this.hasher = hasher;
// this.random = random;
// this.eventDispatcher = eventDispatcher;
// }
constructor(random: ISecureRandom) {
// this.mapper = mapper;
// this.hasher = hasher;
this.random = random;
// this.eventDispatcher = eventDispatcher;
}
public async createCodes(user: User, number: number = 10): Promise<string[]> {
let results: string[] = [];
// this.mapper.deleteCodes(user);
await BackupCode.deleteCodes(user);
// user.twoFactorRecoveryCodes = [""];
// const uid = user.getUID();
for (let i = 1; i <= Math.min(number, 20); i++) {
const code = this.random.generate(BackupCodeStorage.CODE_LENGTH, this.random.CHAR_HUMAN_READABLE);
// const code = crypto
// .randomBytes(Math.ceil(BackupCodeStorage.CODE_LENGTH / 2))
// .toString('hex')
// .slice(0, BackupCodeStorage.CODE_LENGTH);
const dbCode = new BackupCode();
// dbCode.setUserId(uid);
// dbCode.setCode(this.hasher.hash(code));
dbCode.code = await hash.make(code);
// dbCode.setUsed(0);
dbCode.used = false;
// this.mapper.insert(dbCode);
// await dbCode.save();
await dbCode.related('user').associate(user); // speichert schon ab
results.push(code);
}
// this.eventDispatcher.dispatchTyped(new CodesGenerated(user));
return results;
}
public async hasBackupCodes(user: User): Promise<boolean> {
const codes = await user.getBackupCodes();
return codes.length > 0;
}
public async getBackupCodesState(user: User) {
// const codes = this.mapper.getBackupCodes(user);
// const codes = await user.related('backupcodes').query().exec();
const codes: BackupCode[] = await user.getBackupCodes();
const total = codes.length;
let used: number = 0;
codes.forEach((code) => {
if (code.used === true) {
used++;
}
});
return {
enabled: total > 0,
total: total,
used: used,
};
}
// public validateCode(user: User, code: string): boolean {
// const dbCodes = await user.getBackupCodes();
// for (const dbCode of dbCodes) {
// if (parseInt(dbCode.getUsed()) === 0 && this.hasher.verify(code, dbCode.getCode())) {
// dbCode.setUsed(1);
// this.mapper.update(dbCode);
// return true;
// }
// }
// return false;
// }
// public deleteCodes(user: User): void {
// this.mapper.deleteCodes(user);
// }
}
export default BackupCodeStorage;