@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
JavaScript
"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