UNPKG

@sync-in/server

Version:

The secure, open-source platform for file storage, sharing, collaboration, and sync

251 lines (250 loc) 10.6 kB
/* * Copyright (C) 2012-2025 Johan Legrand <johan.legrand@sync-in.com> * This file is part of Sync-in | The open source file sync and share solution * See the LICENSE file for licensing details */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "AuthMethod2FA", { enumerable: true, get: function() { return AuthMethod2FA; } }); const _common = require("@nestjs/common"); const _time2fa = require("time2fa"); const _notifications = require("../../../applications/notifications/constants/notifications"); const _notificationsmanagerservice = require("../../../applications/notifications/services/notifications-manager.service"); const _usersmanagerservice = require("../../../applications/users/services/users-manager.service"); const _constants = require("../../../common/constants"); const _functions = require("../../../common/functions"); const _qrcode = require("../../../common/qrcode"); const _configenvironment = require("../../../configuration/config.environment"); const _cacheservice = require("../../../infrastructure/cache/services/cache.service"); const _auth = require("../../constants/auth"); const _cryptsecret = require("../../utils/crypt-secret"); function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } let AuthMethod2FA = class AuthMethod2FA { async initTwoFactor(user) { const { secret, qrDataUrl } = this.generateSecretAndQr(user.email); // store encrypted secret in cache for 5 minutes await this.cache.set(this.getCacheKey(user.id), this.encryptSecret(secret), 300); return { secret, qrDataUrl }; } async enableTwoFactor(body, req) { // retrieve encrypted secret from cache const secret = await this.cache.get(this.getCacheKey(req.user.id)); if (!secret) { throw new _common.HttpException('The secret has expired', _common.HttpStatus.BAD_REQUEST); } // load user const [auth, user] = await this.verify(body, req, true, secret); if (!auth.success) { throw new _common.HttpException(auth.message, _common.HttpStatus.FORBIDDEN); } // verify user password await this.verifyUserPassword(user, body.password, req.ip); // generate recovery codes const recoveryCodes = this.generateRecoveryCodes(); // store and enable TwoFA & recovery codes await this.usersManager.updateSecrets(user.id, { twoFaSecret: secret, recoveryCodes: recoveryCodes.map((code)=>this.encryptSecret(code)) }); this.sendEmailNotification(req, _constants.ACTION.ADD); return { ...auth, recoveryCodes: recoveryCodes }; } async disableTwoFactor(body, req) { // load user const [auth, user] = await this.verify(body, req, true); if (!auth.success) { throw new _common.HttpException(auth.message, _common.HttpStatus.FORBIDDEN); } // verify user password await this.verifyUserPassword(user, body.password, req.ip); // store and disable TwoFA & recovery codes await this.usersManager.updateSecrets(user.id, { twoFaSecret: undefined, recoveryCodes: undefined }); this.sendEmailNotification(req, _constants.ACTION.DELETE); return auth; } async verify(verifyDto, req, fromLogin = false, secret) { const user = await this.loadUser(req.user.id, req.ip); secret = secret || user.secrets.twoFaSecret; const auth = verifyDto.isRecoveryCode ? await this.validateRecoveryCode(req.user.id, verifyDto.code, user.secrets.recoveryCodes) : this.validateTwoFactorCode(verifyDto.code, secret); this.usersManager.updateAccesses(user, req.ip, auth.success, true).catch((e)=>this.logger.error(`${this.verify.name} - ${e}`)); return fromLogin ? [ auth, user ] : auth; } async adminResetUserTwoFa(userId) { const auth = { success: false, message: '' }; try { await this.usersManager.updateSecrets(userId, { twoFaSecret: undefined, recoveryCodes: undefined }); auth.success = true; } catch (e) { auth.success = false; auth.message = e.message; this.logger.error(`${this.adminResetUserTwoFa.name} - ${e}`); } return auth; } async loadUser(userId, ip) { const user = await this.usersManager.fromUserId(userId); if (!user) { this.logger.warn(`User ${userId} (${ip}) not found`); throw new _common.HttpException(`User not found`, _common.HttpStatus.NOT_FOUND); } this.usersManager.validateUserAccess(user, ip); return user; } async verifyUserPassword(user, password, ip) { // This function works with any authentication method, provided that // the authentication service implements proper user password updates in the database. if (!await this.usersManager.compareUserPassword(user.id, password)) { this.usersManager.updateAccesses(user, ip, false, true).catch((e)=>this.logger.error(`${this.enableTwoFactor.name} - ${e}`)); throw new _common.HttpException('Incorrect code or password', _common.HttpStatus.BAD_REQUEST); } } validateTwoFactorCode(code, encryptedSecret) { const auth = { success: false, message: '' }; if (!encryptedSecret) { auth.message = 'Incorrect code or password'; return auth; } try { auth.success = _time2fa.Totp.validate({ passcode: code, secret: this.decryptSecret(encryptedSecret), drift: 1 }); if (!auth.success) auth.message = 'Incorrect code or password'; } catch (e) { this.logger.error(`${this.validateTwoFactorCode.name} - ${e}`); auth.message = e.message; } return auth; } async validateRecoveryCode(userId, code, encryptedCodes) { const auth = { success: false, message: '' }; if (!encryptedCodes || encryptedCodes.length === 0) { auth.message = 'Invalid code'; } else { try { for (const encCode of encryptedCodes){ if (code === this.decryptSecret(encCode)) { auth.success = true; // removed used code encryptedCodes.splice(encryptedCodes.indexOf(encCode), 1); break; } } if (auth.success) { // update recovery codes await this.usersManager.updateSecrets(userId, { recoveryCodes: encryptedCodes }); } else { auth.message = 'Invalid code'; } } catch (e) { this.logger.error(`${this.validateRecoveryCode.name} - ${e}`); auth.message = e.message; } } return auth; } generateSecretAndQr(userEmail) { // Generate secret + otpauth URL + QR (DataURL) // Totp.generateKey returns { issuer, user, config, secret, url } const key = _time2fa.Totp.generateKey({ issuer: _configenvironment.configuration.auth.mfa.totp.issuer, user: userEmail }, { digits: _auth.TWO_FA_CODE_LENGTH }); const qrDataUrl = (0, _qrcode.qrcodeToDataURL)(key.url); return { secret: key.secret, qrDataUrl: qrDataUrl }; } getCacheKey(userId) { return `${this.cacheKeyPrefix}${userId}`; } encryptSecret(secret) { if (_configenvironment.configuration.auth.encryptionKey) { return (0, _cryptsecret.encryptSecret)(secret, _configenvironment.configuration.auth.encryptionKey); } return secret; } decryptSecret(secret) { if (_configenvironment.configuration.auth.encryptionKey) { return (0, _cryptsecret.decryptSecret)(secret, _configenvironment.configuration.auth.encryptionKey); } return secret; } generateRecoveryCodes(count = 5) { return Array.from({ length: count }, ()=>(0, _functions.generateShortUUID)()); } sendEmailNotification(req, action) { const notification = { app: _notifications.NOTIFICATION_APP.AUTH_2FA, event: _notifications.NOTIFICATION_APP_EVENT.AUTH_2FA[action], element: req.headers['user-agent'], url: req.ip }; this.notificationsManager.sendEmailNotification([ req.user ], notification).catch((e)=>this.logger.error(`${this.sendEmailNotification.name} - ${e}`)); } constructor(cache, usersManager, notificationsManager){ this.cache = cache; this.usersManager = usersManager; this.notificationsManager = notificationsManager; this.logger = new _common.Logger(AuthMethod2FA.name); this.cacheKeyPrefix = 'auth-2fa-pending-user-'; } }; AuthMethod2FA = _ts_decorate([ (0, _common.Injectable)(), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ typeof _cacheservice.Cache === "undefined" ? Object : _cacheservice.Cache, typeof _usersmanagerservice.UsersManager === "undefined" ? Object : _usersmanagerservice.UsersManager, typeof _notificationsmanagerservice.NotificationsManager === "undefined" ? Object : _notificationsmanagerservice.NotificationsManager ]) ], AuthMethod2FA); //# sourceMappingURL=auth-method-two-fa.service.js.map