NestboltNestbolt

@nestbolt/authentication

Quick Start

Set up a complete authentication system in your NestJS application in minutes with @nestbolt/authentication.

This guide walks you through setting up a fully working authentication system from scratch. By the end, you will have login, registration, token refresh, and logout endpoints ready to use.

1. Define Your User Entity

The package expects your user entity to implement the AuthUser interface. Here is the interface for reference:

interface AuthUser {
  id: string;
  name: string;
  email: string;
  password: string;
  emailVerifiedAt: Date | null;
  twoFactorSecret: string | null;
  twoFactorRecoveryCodes: string | null;
  twoFactorConfirmedAt: Date | null;
  passwordConfirmedAt: Date | null;
}

Your entity can have additional fields beyond these. Here is an example using TypeORM:

import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";

@Entity("users")
export class User {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column()
  password: string;

  @Column({ type: "timestamp", nullable: true })
  emailVerifiedAt: Date | null;

  @Column({ type: "text", nullable: true })
  twoFactorSecret: string | null;

  @Column({ type: "text", nullable: true })
  twoFactorRecoveryCodes: string | null;

  @Column({ type: "timestamp", nullable: true })
  twoFactorConfirmedAt: Date | null;

  @Column({ type: "timestamp", nullable: true })
  passwordConfirmedAt: Date | null;

  @CreateDateColumn()
  createdAt: Date;
}

2. Implement the UserRepository

The UserRepository interface is the only required adapter. It has four methods:

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { UserRepository, AuthUser } from "@nestbolt/authentication";
import { User } from "./user.entity";

@Injectable()
export class TypeOrmUserRepository implements UserRepository {
  constructor(
    @InjectRepository(User)
    private readonly repo: Repository<User>,
  ) {}

  async findById(id: string): Promise<AuthUser | null> {
    return this.repo.findOneBy({ id });
  }

  async findByField(field: string, value: string): Promise<AuthUser | null> {
    return this.repo.findOneBy({ [field]: value });
  }

  async save(user: Partial<AuthUser> & { id: string }): Promise<AuthUser> {
    return this.repo.save(user);
  }

  async create(data: Omit<AuthUser, "id">): Promise<AuthUser> {
    const entity = this.repo.create(data);
    return this.repo.save(entity);
  }
}

Mongoose Example

If you are using MongoDB with Mongoose:

import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { UserRepository, AuthUser } from "@nestbolt/authentication";
import { User, UserDocument } from "./user.schema";

@Injectable()
export class MongooseUserRepository implements UserRepository {
  constructor(@InjectModel(User.name) private model: Model<UserDocument>) {}

  async findById(id: string): Promise<AuthUser | null> {
    return this.model.findById(id).lean().exec();
  }

  async findByField(field: string, value: string): Promise<AuthUser | null> {
    return this.model.findOne({ [field]: value }).lean().exec();
  }

  async save(user: Partial<AuthUser> & { id: string }): Promise<AuthUser> {
    return this.model
      .findByIdAndUpdate(user.id, user, { new: true })
      .lean()
      .exec();
  }

  async create(data: Omit<AuthUser, "id">): Promise<AuthUser> {
    return this.model.create(data);
  }
}

Prisma Example

import { Injectable } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { UserRepository, AuthUser } from "@nestbolt/authentication";

@Injectable()
export class PrismaUserRepository implements UserRepository {
  constructor(private prisma: PrismaService) {}

  async findById(id: string): Promise<AuthUser | null> {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async findByField(field: string, value: string): Promise<AuthUser | null> {
    return this.prisma.user.findFirst({ where: { [field]: value } });
  }

  async save(user: Partial<AuthUser> & { id: string }): Promise<AuthUser> {
    const { id, ...data } = user;
    return this.prisma.user.update({ where: { id }, data });
  }

  async create(data: Omit<AuthUser, "id">): Promise<AuthUser> {
    return this.prisma.user.create({ data });
  }
}

3. Implement the CreatesNewUsers Action

For the registration feature to work, you need to provide an implementation of the CreatesNewUsers interface. This action is responsible for validating uniqueness, hashing the password, and creating the user record:

import { Injectable, ConflictException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import * as bcrypt from "bcrypt";
import { CreatesNewUsers, AuthUser } from "@nestbolt/authentication";
import { User } from "./user.entity";

@Injectable()
export class CreateNewUser implements CreatesNewUsers {
  constructor(
    @InjectRepository(User)
    private readonly repo: Repository<User>,
  ) {}

  async create(data: Record<string, any>): Promise<AuthUser> {
    const existing = await this.repo.findOneBy({ email: data.email });
    if (existing) {
      throw new ConflictException("A user with this email already exists.");
    }

    const hashedPassword = await bcrypt.hash(data.password, 12);

    const user = this.repo.create({
      name: data.name,
      email: data.email,
      password: hashedPassword,
      emailVerifiedAt: null,
      twoFactorSecret: null,
      twoFactorRecoveryCodes: null,
      twoFactorConfirmedAt: null,
      passwordConfirmedAt: null,
    });

    return this.repo.save(user);
  }
}

Register this action as a provider using the CREATES_NEW_USERS token:

import { CREATES_NEW_USERS } from "@nestbolt/authentication";

@Module({
  providers: [
    {
      provide: CREATES_NEW_USERS,
      useClass: CreateNewUser,
    },
  ],
})
export class AppModule {}

4. Generate Secrets

You need three secrets for the module configuration:

# JWT signing secret
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

# Refresh token secret (use a different value)
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

# Encryption key (must be exactly 32 bytes, base64-encoded)
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Store these in your .env file:

JWT_SECRET=your_jwt_secret_here
REFRESH_SECRET=your_refresh_secret_here
ENCRYPTION_KEY=your_base64_encryption_key_here

5. Configure the Module

Wire everything together in your application module:

import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import {
  AuthenticationModule,
  Feature,
  CREATES_NEW_USERS,
} from "@nestbolt/authentication";
import { User } from "./users/user.entity";
import { TypeOrmUserRepository } from "./users/typeorm-user.repository";
import { CreateNewUser } from "./users/create-new-user.action";

@Module({
  imports: [
    TypeOrmModule.forRoot({
      // ... your database configuration
    }),
    TypeOrmModule.forFeature([User]),
    AuthenticationModule.forRoot({
      features: [Feature.REGISTRATION],
      userRepository: TypeOrmUserRepository,
      jwtSecret: process.env.JWT_SECRET!,
      refreshSecret: process.env.REFRESH_SECRET!,
      encryptionKey: process.env.ENCRYPTION_KEY!,
      appName: "MyApp",
    }),
  ],
  providers: [
    TypeOrmUserRepository,
    {
      provide: CREATES_NEW_USERS,
      useClass: CreateNewUser,
    },
  ],
})
export class AppModule {}

6. Test It

Start your application and test the endpoints:

Register a New User

curl -X POST http://localhost:3000/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "email": "jane@example.com",
    "password": "secretpassword",
    "passwordConfirmation": "secretpassword"
  }'

Response:

{
  "twoFactor": false,
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Log In

curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "password": "secretpassword"
  }'

Response:

{
  "twoFactor": false,
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Access a Protected Route

Use the access token as a Bearer token in the Authorization header:

curl http://localhost:3000/some-protected-route \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Refresh Tokens

When the access token expires, use the refresh token to get a new pair:

curl -X POST http://localhost:3000/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
  }'

Log Out

curl -X POST http://localhost:3000/logout \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Adding More Features

To enable additional features, add them to the features array and provide any required action implementations. See the Features page for details on what each feature requires.

AuthenticationModule.forRoot({
  features: [
    Feature.REGISTRATION,
    Feature.RESET_PASSWORDS,
    Feature.EMAIL_VERIFICATION,
    Feature.UPDATE_PROFILE_INFORMATION,
    Feature.UPDATE_PASSWORDS,
    Feature.TWO_FACTOR_AUTHENTICATION,
  ],
  userRepository: TypeOrmUserRepository,
  jwtSecret: process.env.JWT_SECRET!,
  refreshSecret: process.env.REFRESH_SECRET!,
  encryptionKey: process.env.ENCRYPTION_KEY!,
  appName: "MyApp",
}),

Next Steps

  • Configuration -- explore all module options including rate limits, token expiry, and two-factor settings.
  • Features -- understand each feature toggle in detail.
  • API Routes -- complete reference for all authentication endpoints.