npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

cqrs-authentication-rbac

v3.3.6

Published

authentication service base on cqrs and rbac authorization

Downloads

136

Readme

CQRS Authentication RBAC Module for NestJS

This module provides Role-Based Access Control (RBAC) authentication using CQRS in a NestJS application. It integrates with PostgreSQL and uses JWT for authentication.

Features

Authentication Features

  • User Registration – Allow users to sign up with email/password authentication
  • User Login – Authenticate users with JWT-based authentication
  • Token Management – Use access & refresh tokens with configurable expiration
  • Password Reset & Recovery – Enable users to recover their accounts
  • Session Management – Handle multiple active sessions & logout
  • User Status Management - Track and manage user account status (active, inactive, suspended)

Role-Based Access Control (RBAC) Features

  • Role Management – Create, update, and delete roles (e.g., Admin, User, Editor)
  • Permission Management – Assign permissions to roles (e.g., read, write, delete)
  • User-Role Assignment – Map users to one or multiple roles
  • Fine-Grained Access Control – Restrict access based on permissions
  • Middleware/Guards – Enforce role-based access in API endpoints
  • Endpoint-Based Authorization - Control access to specific API endpoints

Security & Compliance

  • Secure Password Hashing – Use bcrypt for password hashing
  • Rate Limiting & Brute Force Protection – Prevent login abuse
  • Audit Logging – Track authentication & authorization actions
  • RBAC Configuration Storage – Store roles/permissions in PostgreSQL
  • Granular Access Policies – Allow resource-level access control
  • Error Handling & Logging - Comprehensive error handling and logging system

Installation

Ensure you have NestJS installed in your project. Then, install the required dependencies:

npm i cqrs-authentication-rbac

Usage

Register the Module

Import and configure the module in your NestJS application:

import { Module } from '@nestjs/common';
import { CQRSAuthenticationRBAC } from 'cqrs-authentication-rbac';

@Module({
  imports: [
    CQRSAuthenticationRBAC.register({
      dbConf: {
        host: process.env.PG_MAIN_DB_HOST || 'localhost',
        port: parseInt(process.env.PG_MAIN_DB_PORT || '5432', 10),
        user: process.env.PG_MAIN_DB_USER || 'postgres',
        password: process.env.PG_MAIN_DB_PASSWORD || 'postgres',
        database: process.env.PG_MAIN_DB_DATABASE || 'postgres',
      },
      jwtOptions: {
        secret: process.env.JWT_SECRET || 'defaultSecret',
        signOptions: { expiresIn: '60s' },
      },
      rbacConf: {
        authSecretKey: process.env.AUTH_SECRET_KEY || 'defaultAuthSecretKey',
        authSalt: process.env.AUTH_SALT || 'defaultAuthSalt',
        authJwtSecret: process.env.AUTH_JWT_SECRET || 'defaultAuthJwtSecret',
        authAccessTokenSecretKey:
          process.env.AUTH_ACCESS_TOKEN_SECRET_KEY ||
          'defaultAccessTokenSecretKey',
        authRefreshTokenSecretKey:
          process.env.AUTH_REFRESH_TOKEN_SECRET_KEY ||
          'defaultRefreshTokenSecretKey',
        authAccessTokenExpiresIn:
          process.env.AUTH_ACCESS_TOKEN_EXPIRES_IN || '3600s',
        authRefreshTokenExpiresIn:
          process.env.AUTH_REFRESH_TOKEN_EXPIRES_IN || '86400s',
        authTokenType: process.env.AUTH_TOKEN_TYPE ?? 'Bearer',
        defaultUserStatus: 'ACTIVE',
      },
      migrations: {
        enable: true,
        migrationTableName: '_migration_authentication',
      },
      constroller: {
        enable: true,
      },
    }),
  ],
})
export class AppModule {}

export interface IRBACConf {
  authSecretKey: string;
  authSalt: string;
  authJwtSecret: string;
  authAccessTokenSecretKey: string;
  authRefreshTokenSecretKey: string;
  authAccessTokenExpiresIn: string;
  authRefreshTokenExpiresIn: string;
  authTokenType: string;
  defaultUserStatus: UserStatus;

  // Config google authentication
  authGoogleClientId?: string;
  authGoogleClientSecret?: string;

  // Multi-Factor Authentication (MFA)
  mfa?: {
    enable?: boolean;
    template?: string;
    method?: MfaMethod;
    otpLength?: number;
    notifyProxy?: INotifyProxy;
  };
}

export interface AuthRBACConfig {
  dbConf: PoolConfig;
  jwtOptions: JwtModuleOptions;
  rbacConf: IRBACConf;
  migrations?: {
    enable?: boolean;
    migrationTableName?: string;
  };
  constroller?: {
    enable?: boolean;
  };
}
@Module({})
export class CQRSAuthenticationRBAC implements OnModuleInit {
  private static config: AuthRBACConfig;

  static register(conf: AuthRBACConfig): DynamicModule {
    this.config = conf;
    return {
      module: CQRSAuthenticationRBAC,
      imports: [
        CacheModule.register(),
        ConfigModule.forRoot({
          load: [
            () => ({
              authRBACConfig: conf,
              GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
            }),
          ],
        }),
        CqrsModule,
        PostgresModule.forRootAsync({
          useFactory: () => [
            {
              name: CONNECTION_STRING_DEFAULT,
              ...conf.dbConf,
            },
          ],
        }),
        PostgresModule.forFeature(CONNECTION_STRING_DEFAULT),
        JwtModule.register(conf.jwtOptions),
      ],
      providers: [
        ...Handlers,
        ...Repositories,
        {
          provide: APP_INTERCEPTOR,
          useClass: ErrorInterceptor,
        },
        {
          provide: APP_INTERCEPTOR,
          useClass: LoggingInterceptor,
        },
        {
          provide: APP_PIPE,
          useClass: ValidationPipe,
        },
        AuthConf,
      ],
      exports: [...Handlers, ...Repositories, AuthConf],
      controllers: conf.constroller?.enable ? Controllers : [],
    };
  }

  async onModuleInit() {
    // TODO: How exec right migration file directory
    if (CQRSAuthenticationRBAC.config.migrations?.enable) {
      const migrateExecution = new PgMigration(
        CQRSAuthenticationRBAC.config.dbConf,
        {
          modulePrefix: __dirname + '/infrastructure/migrations/',
          migrationTableName:
            CQRSAuthenticationRBAC.config.migrations?.migrationTableName ??
            '_mig_auth_rbac',
        },
      );
      await migrateExecution.executeMigrations();
    }
  }
}

Available Bussiness logic

  • Let's me write doc after finish verion 4.0.0
@Controller('auth')
export class AuthController implements ICustomController {
  @Inject() private readonly commandBus: CommandBus;
  @Inject() private readonly queryBus: QueryBus;

  @Post('login')
  @HttpCode(200)
  login(@Body() loginDto: LoginDTO) {
    return this.queryBus.execute(
      new LoginQuery(loginDto.username, loginDto.password),
    );
  }

  @Post('google-login')
  @HttpCode(200)
  async googleLogin(@Body() dto: GoogleLoginDTO) {
    return this.commandBus.execute(new GoogleLoginCommand(dto.token));
  }

  @Post('register')
  register(@Body() dto: CreateUserDTO) {
    return this.commandBus.execute(
      new CreateUserCommand(
        dto.username,
        dto.password,
        dto.mfa,
        UserType.PASSWORD,
        dto.metadata,
      ),
    );
  }

  @UseGuards(AccessTokenGuard)
  @ApiBearerAuth('access-token')
  @UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
  @Post('refresh-token')
  @HttpCode(201)
  refreshToken(@Body() dto: RefreshTokenDTO) {
    return this.commandBus.execute(new RefreshTokenCommand(dto.refreshToken));
  }

  @Get('me')
  @UseGuards(AccessTokenGuard)
  @ApiBearerAuth('access-token')
  @UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
  me(@Headers('authorization') authorization: string) {
    const accessToken = extractTokenFromHeader(authorization);
    return this.queryBus.execute(new MeQuery(accessToken));
  }

  // use when register
  @Post('verify-register-mfa-session')
  @HttpCode(200)
  async verifyMfaSession(@Body() dto: VerifyMfaSessionDTO) {
    return this.commandBus.execute(
      new VerifyMfaSessionCommand(dto.sessionId, dto.otp),
    );
  }

  @Post('verify-login-mfa-session')
  @HttpCode(200)
  async verifyLoginMfaSession(@Body() dto: VerifyLoginMfaSessionDTO) {
    return this.commandBus.execute(
      new VerifyLoginMfaSessionCommand(dto.sessionId, dto.otp),
    );
  }
}

@Controller('actions')
@UseGuards(AccessTokenGuard)
@ApiBearerAuth('access-token')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
export class ActionController {
  @Inject() private readonly commandBus: CommandBus;

  @Post()
  @HttpCode(201)
  async create(@Body() dto: CreateActionDTO[]) {
    return this.commandBus.execute(new CreateActionsCommand(dto));
  }

  @Put()
  @HttpCode(200)
  async update(@Body() dto: UpdateActionDTO[]) {
    return this.commandBus.execute(new UpdateActionsCommand(dto));
  }

  @Delete()
  @HttpCode(204)
  async delete(@Body() dto: DeleteActionDTO) {
    await this.commandBus.execute(new DeleteActionsCommand(dto.ids));
  }
}

@Controller('endpoints')
@UseGuards(AccessTokenGuard)
@ApiBearerAuth('access-token')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
export class EndpointController {
  @Inject() private readonly commandBus: CommandBus;
  @Inject() private readonly queryBus: QueryBus;

  @Post()
  @HttpCode(201)
  @ApiOperation({ summary: 'Create new endpoints' })
  @ApiResponse({
    status: 201,
    description: 'The endpoints have been successfully created.',
    schema: {
      example: [
        {
          path: '/api/example',
          method: 'GET',
          metadata: { key: 'value' },
          status: 'ACTIVE',
        },
      ],
    },
  })
  async create(@Body() dto: CreateEndpointDTO[]) {
    return this.commandBus.execute(new CreateEndpointsCommand(dto));
  }

  @Put()
  @HttpCode(200)
  async update(@Body() dto: UpdateEndpointDTO[]) {
    return this.commandBus.execute(new UpdateEndpointsCommand(dto));
  }

  @Delete()
  @HttpCode(204)
  async delete(@Body() dto: DeleteEndpointDTO) {
    await this.commandBus.execute(new DeleteEndpointsCommand(dto.ids));
  }
}

@ApiTags('Roles')
@Controller('roles')
@UseGuards(AccessTokenGuard)
@ApiBearerAuth('access-token')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
export class RoleController {
  @Inject() private readonly commandBus: CommandBus;
  @Inject() private readonly queryBus: QueryBus;

  @Post()
  @HttpCode(201)
  async create(@Body() dto: CreateRoleDto) {
    return this.commandBus.execute(
      new CreateRoleCommand(
        dto.name,
        dto.description,
        dto.status,
        dto.metadata,
      ),
    );
  }

  @Post('assign-user')
  @HttpCode(200)
  async assignRoleToUser(@Body() dto: AssignRoleToUserDto) {
    return this.commandBus.execute(
      new AssignRoleToUserCommand(dto.roleIds, dto.userIds),
    );
  }

  @Post('assign-actions')
  @ApiOperation({ summary: 'Assign actions to roles' })
  @ApiBody({ type: AssignActionsToRoleDto })
  async assignActionsToRole(
    @Body() dto: AssignActionsToRoleDto,
  ): Promise<void> {
    const { actionIds, roleIds } = dto;
    await this.commandBus.execute(
      new AssignActionToRoleCommand(actionIds, roleIds),
    );
  }

  @Post('assign-endpoints')
  @ApiOperation({ summary: 'Assign endpoints to roles' })
  @ApiBody({ type: AssignEndpointsToRoleDto })
  async assignEndpointsToRole(
    @Body() dto: AssignEndpointsToRoleDto,
  ): Promise<void> {
    const { endpointIds, roleIds } = dto;
    await this.commandBus.execute(
      new AssignEndpointToRoleCommand(endpointIds, roleIds),
    );
  }
}

@Injectable()
export class AccessTokenGuard implements CanActivate {
  @Inject() authenticationConfig: AuthConf;

  private readonly logger = new Logger(AccessTokenGuard.name);
  constructor(
    private readonly jwtService: JwtService,
    private readonly queryBus: QueryBus,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    if (isTestMode()) {
      this.logger.debug('Test mode, skipping access token validation');
      return true;
    }
    const request: Request = context.switchToHttp().getRequest();
    const currentPath = request.path;
    const currentMethod = request.method;
    /// Is public route
    const isPublicRoutes = await this.queryBus.execute(
      new IsPublicRoutesQuery(currentPath, currentMethod),
    );
    if (isPublicRoutes) {
      return true;
    }

    // Check if the request has an authorization header
    const token = extractTokenFromHeader(
      request.headers.authorization,
      this.authenticationConfig.getRbacConf().authTokenType,
    );
    if (!token) {
      throw new UnauthorizedException('Access token is missing');
    }

    // Decode the token
    let decoded = {
      uid: '',
      roles: [],
    };
    try {
      // Verify JWT token manually
      decoded = this.jwtService.verify(token, {
        secret:
          this.authenticationConfig.getRbacConf().authAccessTokenSecretKey,
      });
      request['user'] = decoded;
    } catch (e) {
      throw new UnauthorizedException('Invalid or expired token');
    }

    // Can exec route
    const canAccess = await this.queryBus.execute(
      new CanExecRouteQuery(
        decoded['uid'],
        currentPath,
        currentMethod,
        decoded['roles'],
      ),
    );
    if (!canAccess) {
      throw new ForbiddenException(
        'You do not have permission to access this route',
      );
    }

    return true;
  }
}

Database

module.exports = async (client, schema) => {
  await client.query(`
    CREATE TABLE IF NOT EXISTS ${schema}."auth_users" (
      id VARCHAR(255) PRIMARY KEY,
      username VARCHAR(255) UNIQUE NOT NULL,
      password_hash TEXT NOT NULL,
      status VARCHAR(50),
      type VARCHAR(50),
      mfa JSONB DEFAULT '{}',
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW()
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_roles" (
      id VARCHAR(255) PRIMARY KEY,
      name VARCHAR(255) UNIQUE NOT NULL,
      description TEXT,
      status VARCHAR(50),
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW()
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_user_roles" (
      user_id VARCHAR(255) REFERENCES ${schema}."auth_users"(id) ON DELETE CASCADE,
      role_id VARCHAR(255) REFERENCES ${schema}."auth_roles"(id) ON DELETE CASCADE,
      status VARCHAR(50),
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW(),
      PRIMARY KEY (user_id, role_id)
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_actions" (
      id VARCHAR(255) PRIMARY KEY,
      name VARCHAR(255) UNIQUE NOT NULL,
      description TEXT,
      status VARCHAR(50),
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW()
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_endpoints" (
      id VARCHAR(255) PRIMARY KEY,
      path VARCHAR(255) NOT NULL,
      method VARCHAR(10) NOT NULL,
      metadata JSONB DEFAULT '{}',
      status VARCHAR(50),
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW(),
      CONSTRAINT unique_path_method UNIQUE (path, method)
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_role_action_permissions" (
      role_id VARCHAR(255) REFERENCES ${schema}."auth_roles"(id) ON DELETE CASCADE,
      action_id VARCHAR(255) REFERENCES ${schema}."auth_actions"(id) ON DELETE CASCADE,
      status VARCHAR(50),
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW(),
      PRIMARY KEY (role_id, action_id)
    );

    CREATE TABLE IF NOT EXISTS ${schema}."auth_role_endpoint_permissions" (
      role_id VARCHAR(255) REFERENCES ${schema}."auth_roles"(id) ON DELETE CASCADE,
      endpoint_id VARCHAR(255) REFERENCES ${schema}."auth_endpoints"(id) ON DELETE CASCADE,
      status VARCHAR(50),
      metadata JSONB DEFAULT '{}',
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW(),
      PRIMARY KEY (role_id, endpoint_id)
    );
  `);

  await client.query(`
    ALTER TABLE ${schema}."auth_users"
    ADD COLUMN IF NOT EXISTS mfa JSONB DEFAULT '{}';
  `);

  await client.query(`
    ALTER TABLE ${schema}."auth_users"
    ADD COLUMN IF NOT EXISTS reset_password JSONB DEFAULT '{}';
  `);
};

License

MIT