From 3a8c7045ee9aa9be03dc7e87daff81d15404c89e Mon Sep 17 00:00:00 2001 From: khatabwedaa Date: Wed, 1 Apr 2026 13:44:56 +0300 Subject: [PATCH 1/3] Fix forRootAsync DI bypass and TwoFactorController empty body crash forRootAsync was using `new options.userRepository()` which bypassed NestJS dependency injection, causing repositories to have undefined dependencies (e.g. DataSource). Now uses ModuleRef.create() to properly instantiate repositories through the DI container. Also adds conditional PASSWORD_RESET_REPOSITORY registration that was missing from forRootAsync. TwoFactorController.enable() crashed with TypeError when called without a request body because @Body() returned undefined. Now handles optional body with safe navigation. Co-Authored-By: Claude Opus 4.6 --- src/authentication.module.ts | 46 +++++++++++++++------ src/controllers/two-factor.controller.ts | 4 +- test/authentication.module.spec.ts | 51 ++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/authentication.module.ts b/src/authentication.module.ts index d6d9d5e..d5f3d82 100644 --- a/src/authentication.module.ts +++ b/src/authentication.module.ts @@ -1,4 +1,5 @@ import { DynamicModule, Module, Provider, Type } from "@nestjs/common"; +import { ModuleRef } from "@nestjs/core"; import { JwtModule } from "@nestjs/jwt"; import { PassportModule } from "@nestjs/passport"; import { @@ -154,6 +155,32 @@ export class AuthenticationModule { } static forRootAsync(asyncOptions: AuthenticationAsyncOptions): DynamicModule { + const optionsProvider: Provider = { + provide: AUTHENTICATION_OPTIONS, + useFactory: asyncOptions.useFactory, + inject: asyncOptions.inject ?? [], + }; + + const repositoryProviders: Provider[] = [ + { + provide: USER_REPOSITORY, + useFactory: async (options: AuthenticationModuleOptions, moduleRef: ModuleRef) => + moduleRef.create(options.userRepository), + inject: [AUTHENTICATION_OPTIONS, ModuleRef], + }, + { + provide: PASSWORD_RESET_REPOSITORY, + useFactory: async (options: AuthenticationModuleOptions, moduleRef: ModuleRef) => + options.passwordResetRepository + ? moduleRef.create(options.passwordResetRepository) + : null, + inject: [AUTHENTICATION_OPTIONS, ModuleRef], + }, + ]; + + // With async options, controllers/services are registered eagerly since + // the features array is not available at static definition time. + // The FeatureEnabledGuard on each controller gates access at runtime. return { module: AuthenticationModule, global: true, @@ -184,25 +211,18 @@ export class AuthenticationModule { TwoFactorChallengeController, ], providers: [ - { - provide: AUTHENTICATION_OPTIONS, - useFactory: asyncOptions.useFactory, - inject: asyncOptions.inject ?? [], - }, - { - provide: USER_REPOSITORY, - useFactory: (options: AuthenticationModuleOptions) => { - return new options.userRepository(); - }, - inject: [AUTHENTICATION_OPTIONS], - }, + optionsProvider, + ...repositoryProviders, + // Core services AuthService, EncryptionService, RecoveryCodeService, ConfirmPasswordService, + // Passport strategies LocalStrategy, JwtStrategy, JwtRefreshStrategy, + // Guards JwtAuthGuard, LoginThrottleGuard, TwoFactorThrottleGuard, @@ -210,7 +230,9 @@ export class AuthenticationModule { FeatureEnabledGuard, PasswordConfirmedGuard, GuestGuard, + // Interceptors CanonicalizeUsernameInterceptor, + // Feature services (gated at runtime by FeatureEnabledGuard) RegistrationService, PasswordResetService, EmailVerificationService, diff --git a/src/controllers/two-factor.controller.ts b/src/controllers/two-factor.controller.ts index 075a3fd..8959181 100644 --- a/src/controllers/two-factor.controller.ts +++ b/src/controllers/two-factor.controller.ts @@ -25,8 +25,8 @@ export class TwoFactorController { @Post("two-factor-authentication") @UseGuards(PasswordConfirmedGuard) @HttpCode(HttpStatus.OK) - async enable(@CurrentUser() user: AuthUser, @Body() body: { force?: boolean }) { - await this.twoFactorService.enable(user, body.force ?? false); + async enable(@CurrentUser() user: AuthUser, @Body() body?: { force?: boolean }) { + await this.twoFactorService.enable(user, body?.force ?? false); return { message: "Two-factor authentication enabled." }; } diff --git a/test/authentication.module.spec.ts b/test/authentication.module.spec.ts index 2aebeea..f0e376f 100644 --- a/test/authentication.module.spec.ts +++ b/test/authentication.module.spec.ts @@ -1,4 +1,5 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; +import { ModuleRef } from "@nestjs/core"; import { AuthenticationModule } from "../src/authentication.module"; import { Feature } from "../src/interfaces"; import { AuthController } from "../src/controllers/auth.controller"; @@ -384,7 +385,7 @@ describe("AuthenticationModule", () => { expect(userRepoProvider.inject).toContain(AUTHENTICATION_OPTIONS); }); - it("should create user repository instance from options", () => { + it("should create user repository instance via ModuleRef", async () => { const result = AuthenticationModule.forRootAsync({ useFactory: () => baseOptions, }); @@ -393,8 +394,52 @@ describe("AuthenticationModule", () => { const userRepoProvider = providers.find( (p: any) => typeof p === "object" && p.provide === USER_REPOSITORY, ); - const instance = userRepoProvider.useFactory(baseOptions); + expect(userRepoProvider.inject).toContain(ModuleRef); + + const mockModuleRef = { + create: vi.fn().mockResolvedValue(new MockUserRepository()), + }; + const instance = await userRepoProvider.useFactory(baseOptions, mockModuleRef); expect(instance).toBeInstanceOf(MockUserRepository); + expect(mockModuleRef.create).toHaveBeenCalledWith(MockUserRepository); + }); + + it("should provide PASSWORD_RESET_REPOSITORY via ModuleRef when configured", async () => { + const optsWithReset = { + ...baseOptions, + passwordResetRepository: MockPasswordResetRepository as any, + }; + const result = AuthenticationModule.forRootAsync({ + useFactory: () => optsWithReset, + }); + const providers = result.providers as any[]; + + const resetRepoProvider = providers.find( + (p: any) => typeof p === "object" && p.provide === PASSWORD_RESET_REPOSITORY, + ); + expect(resetRepoProvider).toBeDefined(); + + const mockModuleRef = { + create: vi.fn().mockResolvedValue(new MockPasswordResetRepository()), + }; + const instance = await resetRepoProvider.useFactory(optsWithReset, mockModuleRef); + expect(instance).toBeInstanceOf(MockPasswordResetRepository); + expect(mockModuleRef.create).toHaveBeenCalledWith(MockPasswordResetRepository); + }); + + it("should return null for PASSWORD_RESET_REPOSITORY when not configured", async () => { + const result = AuthenticationModule.forRootAsync({ + useFactory: () => baseOptions, + }); + const providers = result.providers as any[]; + + const resetRepoProvider = providers.find( + (p: any) => typeof p === "object" && p.provide === PASSWORD_RESET_REPOSITORY, + ); + const mockModuleRef = { create: vi.fn() }; + const instance = await resetRepoProvider.useFactory(baseOptions, mockModuleRef); + expect(instance).toBeNull(); + expect(mockModuleRef.create).not.toHaveBeenCalled(); }); it("should use empty inject array when inject is not provided", () => { From a5f5b47bc2131a7def8970d5bd8ffb76870fc05d Mon Sep 17 00:00:00 2001 From: khatabwedaa Date: Wed, 1 Apr 2026 13:46:31 +0300 Subject: [PATCH 2/3] Bump version to 0.1.1 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b2722..ccaf6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `@nestbolt/authentication` will be documented in this file. +## 0.1.1 + +### Bug Fixes + +- **forRootAsync DI bypass** — `forRootAsync` was using `new options.userRepository()` which bypassed NestJS dependency injection, leaving repository dependencies (e.g. `DataSource`) undefined. Now uses `ModuleRef.create()` to properly instantiate repositories through the DI container. +- **forRootAsync missing PASSWORD_RESET_REPOSITORY** — The conditional `PASSWORD_RESET_REPOSITORY` registration present in `forRoot` was absent from `forRootAsync`. Now registers it via `ModuleRef.create()` when configured, or `null` when not (compatible with `@Optional()` injection). +- **TwoFactorController.enable() crash on empty body** — Calling `POST /user/two-factor-authentication` without a request body caused a `TypeError` because `@Body()` returned `undefined`. Now handles optional body with safe navigation (`body?.force ?? false`). + ## 0.1.0 ### Features diff --git a/package.json b/package.json index 1059f0b..d9eb38e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nestbolt/authentication", - "version": "0.1.0", + "version": "0.1.1", "packageManager": "pnpm@9.15.4", "description": "Frontend-agnostic authentication backend for NestJS with 2FA, password reset, email verification, and more", "main": "dist/index.js", From 64aa1511a97ede3ea4b5a67f563cf16e5d95b4d4 Mon Sep 17 00:00:00 2001 From: khatabwedaa Date: Wed, 1 Apr 2026 13:46:53 +0300 Subject: [PATCH 3/3] Fix changelog version formatting and update bug fix descriptions --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccaf6e9..5ab33ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to `@nestbolt/authentication` will be documented in this file. -## 0.1.1 +## v0.1.1 ### Bug Fixes @@ -10,7 +10,7 @@ All notable changes to `@nestbolt/authentication` will be documented in this fil - **forRootAsync missing PASSWORD_RESET_REPOSITORY** — The conditional `PASSWORD_RESET_REPOSITORY` registration present in `forRoot` was absent from `forRootAsync`. Now registers it via `ModuleRef.create()` when configured, or `null` when not (compatible with `@Optional()` injection). - **TwoFactorController.enable() crash on empty body** — Calling `POST /user/two-factor-authentication` without a request body caused a `TypeError` because `@Body()` returned `undefined`. Now handles optional body with safe navigation (`body?.force ?? false`). -## 0.1.0 +## v0.1.0 ### Features