UNPKG

@nestjs-mod/two-factor

Version:

Two factor module with an error filter, guard, controller, database migrations and rest-sdk for work with module from other nodejs appliaction

265 lines 11.5 kB
"use strict"; var TwoFactorService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.TwoFactorService = void 0; const tslib_1 = require("tslib"); const common_1 = require("@nestjs/common"); const prisma_1 = require("@nestjs-mod/prisma"); const OTPAuth = tslib_1.__importStar(require("otpauth")); const prisma_client_1 = require("./generated/prisma-client"); const two_factor_events_service_1 = require("./two-factor-events.service"); const two_factor_configuration_1 = require("./two-factor.configuration"); const two_factor_constants_1 = require("./two-factor.constants"); const two_factor_errors_1 = require("./two-factor.errors"); // const TOTP_PERIOD = 30; let TwoFactorService = TwoFactorService_1 = class TwoFactorService { constructor(prismaClient, twoFactorConfiguration, twoFactorEventsService) { this.prismaClient = prismaClient; this.twoFactorConfiguration = twoFactorConfiguration; this.twoFactorEventsService = twoFactorEventsService; this.logger = new common_1.Logger(TwoFactorService_1.name); } async getTotp(options) { return new OTPAuth.TOTP({ // Provider or service the account is associated with. issuer: 'SSO', // Account identifier. label: options.username || 'Account', // Algorithm used for the HMAC function, possible values are: // "SHA1", "SHA224", "SHA256", "SHA384", "SHA512", // "SHA3-224", "SHA3-256", "SHA3-384" and "SHA3-512". algorithm: 'SHA1', // Length of the generated tokens. digits: 6, // Interval of time for which a token is valid, in seconds. ...(options.timeout ? { period: Math.floor(options.timeout / 1000) } : {}), // Arbitrary key encoded in base32 or `OTPAuth.Secret` instance // (if omitted, a cryptographically secure random secret is generated). secret: OTPAuth.Secret.fromBase32(options.secret), // or: `OTPAuth.Secret.fromBase32("US3WHSG7X5KAPV27VANWKQHF3SH3HULL")` // or: `new OTPAuth.Secret()` }); } async generateCode(options) { const twoFactorUser = await this.getOrCreateUser({ ...options, username: options.externalUsername, }); await this.removeOutdateTwoFactorCode(options); let timeout = 0; if (this.twoFactorConfiguration.getTimeoutValue) { timeout = await this.twoFactorConfiguration.getTimeoutValue({ twoFactorUser, }); } const code = String((await this.getTotp({ username: twoFactorUser.username || undefined, secret: twoFactorUser.secret, timeout, })).generate()); const existsUsedCode = await this.prismaClient.twoFactorCode.findFirst({ where: { code, externalTenantId: options.externalTenantId, TwoFactorUser: { externalUserId: options.externalUserId, externalTenantId: options.externalTenantId, }, used: false, }, }); if (existsUsedCode?.used) { throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodePleaseWaitXXSeconds, undefined, { timeout }); } const existsOutdatedCode = await this.prismaClient.twoFactorCode.findFirst({ where: { externalTenantId: options.externalTenantId, operationName: options.operationName, type: options.type, TwoFactorUser: { externalUserId: options.externalUserId, externalTenantId: options.externalTenantId, }, used: false, outdated: true, }, }); if (existsOutdatedCode) { this.logger.debug(existsOutdatedCode); await this.prismaClient.twoFactorCode.updateMany({ data: { used: true }, where: { externalTenantId: options.externalTenantId, operationName: options.operationName, type: options.type, TwoFactorUser: { externalUserId: options.externalUserId, externalTenantId: options.externalTenantId, }, used: false, outdated: true, }, }); throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodePleaseWaitXXSeconds, undefined, { timeout }); } // disable old codes await this.prismaClient.twoFactorCode.updateMany({ data: { outdated: true }, where: { externalTenantId: options.externalTenantId, operationName: options.operationName, type: options.type, TwoFactorUser: { externalUserId: options.externalUserId, externalTenantId: options.externalTenantId, }, }, }); try { const twoFactorCode = await this.prismaClient.twoFactorCode.create({ include: { TwoFactorUser: true }, data: { code, externalTenantId: options.externalTenantId, operationName: options.operationName, type: options.type, TwoFactorUser: { connect: { externalTenantId_externalUserId: { externalUserId: options.externalUserId, externalTenantId: options.externalTenantId, }, }, }, used: false, outdated: false, }, }); await this.twoFactorEventsService.send({ GenerateCode: { twoFactorUser, twoFactorCode, code, repetition: Boolean(options.repetition), }, externalTenantId: options.externalTenantId, externalUserId: options.externalUserId, operationName: options.operationName, type: options.type, }); return { twoFactorUser, twoFactorCode, twoFactorTimeout: timeout }; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) { this.logger.error(err, err.stack); throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodePleaseWaitXXSeconds, undefined, { timeout }); } } async validateCode(options) { let twoFactorCode = await this.prismaClient.twoFactorCode.findFirst({ include: { TwoFactorUser: true }, where: { code: { equals: options.code }, externalTenantId: options.externalTenantId, }, }); if (!twoFactorCode) { throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodeNotSet); } if (twoFactorCode.outdated) { throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodeIsOutdated); } if (twoFactorCode.used) { throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodeIsUsed); } let timeout = 0; if (this.twoFactorConfiguration.getTimeoutValue) { timeout = await this.twoFactorConfiguration.getTimeoutValue({ twoFactorCode, twoFactorUser: twoFactorCode.TwoFactorUser, }); const totp = await this.getTotp({ secret: twoFactorCode.TwoFactorUser.secret, username: twoFactorCode.TwoFactorUser.username || undefined, timeout, }); const tokenIsValid = totp.validate({ token: options.code, window: 1, }) !== null; if (!tokenIsValid) { throw new two_factor_errors_1.TwoFactorError(two_factor_errors_1.TwoFactorErrorEnum.TwoFactorCodeIsOutdated); } } twoFactorCode = await this.prismaClient.twoFactorCode.update({ include: { TwoFactorUser: true }, data: { used: true, }, where: { id: twoFactorCode.id, externalTenantId: options.externalTenantId, }, }); return { twoFactorUser: twoFactorCode.TwoFactorUser, twoFactorCode, twoFactorTimeout: timeout }; } async getOrCreateUser(options) { const secret = new OTPAuth.Secret({ size: 20 }); return await this.prismaClient.twoFactorUser.upsert({ create: { externalTenantId: options.externalTenantId, externalUserId: options.externalUserId, secret: secret.base32, username: options.username, }, update: {}, where: { externalTenantId_externalUserId: { externalTenantId: options.externalTenantId, externalUserId: options.externalUserId, }, }, }); } async removeOutdateTwoFactorCode(options) { const twoFactorCodes = await this.prismaClient.twoFactorCode.findMany({ include: { TwoFactorUser: true }, where: { TwoFactorUser: { externalTenantId: { equals: options.externalTenantId }, externalUserId: { equals: options.externalUserId }, }, used: { equals: false }, externalTenantId: options.externalTenantId, }, }); const itemIdsForDelete = (await Promise.all(twoFactorCodes.map(async (twoFactorCode) => this.twoFactorConfiguration.getTimeoutValue && +new Date() - +twoFactorCode.updatedAt > (await this.twoFactorConfiguration.getTimeoutValue({ twoFactorCode, twoFactorUser: twoFactorCode.TwoFactorUser, })) ? twoFactorCode : undefined))) .filter(Boolean) .map((item) => item?.id); await this.prismaClient.twoFactorCode.updateMany({ data: { outdated: true, }, where: { id: { in: itemIdsForDelete }, externalTenantId: options.externalTenantId, }, }); } }; exports.TwoFactorService = TwoFactorService; exports.TwoFactorService = TwoFactorService = TwoFactorService_1 = tslib_1.__decorate([ (0, common_1.Injectable)(), tslib_1.__param(0, (0, prisma_1.InjectPrismaClient)(two_factor_constants_1.TWO_FACTOR_FEATURE)), tslib_1.__metadata("design:paramtypes", [prisma_client_1.PrismaClient, two_factor_configuration_1.TwoFactorConfiguration, two_factor_events_service_1.TwoFactorEventsService]) ], TwoFactorService); //# sourceMappingURL=two-factor.service.js.map