@sync-in/server
Version:
The secure, open-source platform for file storage, sharing, collaboration, and sync
251 lines (250 loc) • 10.6 kB
JavaScript
/*
* 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