JWT Authentication with NestJS

Introduction

In this post, I’ll show you how to implement a REST Api with the following endpoints:

POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/posts (authenticated user)
DELETE /api/v1/posts (admin only)

Current node version I’m using: v20.12.1

Current NestJS version I’m using: 10.4.9


Auth Service

Create new project

First, let’s create the auth service, which will handle login, registration, and user information like roles. nest new auth --strict

Add database and ORM dependencies

Now, we need to configure the database. In this case, we’re using TypeORM and PostgreSQL. Let’s install the necessary dependencies: npm install --save @nestjs/typeorm typeorm pg

// Filepath: ./src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'username',
      password: 'password',
      database: 'databaseName',
      synchronize: true,
      autoLoadEntities: true,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

To keep our credentials secure, we’ll use dotenv to manage environment variables through a .env file:

npm install dotenv

Our .env file will look like this:

# Filepath: .env
DB_URL=localhost
DB_PORT=5432
DB_USER=username
DB_PASS=password
DB_NAME=databaseName

Update the AppModule to use environment variables:

// Filepath: ./src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import 'dotenv/config';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
      synchronize: true,
      autoLoadEntities: true,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Create Auth and Users modules

Next, we create the auth and users modules:

nest g module auth
nest g controller auth --no-spec
nest g service auth --no-spec
nest g module users
nest g service users --no-spec

Inside the users folder, we should create an ’entities’ folder and then add the user.entity.ts file:

// Filepath: .src/users/entities/user.entity.ts
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  email: string;
  @Column()
  username: string;
  @Column({ name: 'password_hash' })
  passwordHash: string;
}

In the src/users/ folder, we should also create the user repository:

// Filepath: .src/users/user.repository.ts
import { DataSource, Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserRepository extends Repository<User> {
  constructor(dataSource: DataSource) {
    super(User, dataSource.createEntityManager());
  }
}

Next, we need to update the UsersModule (src/users/users.module.ts), adding TypeOrmModule.forFeature([User]) in imports and UserRepository in providers. The file should look like this:

// Filepath: .src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService, UserRepository],
  exports: [UsersService],
})
export class UsersModule {}

Similarly, we need to update the AuthModule (src/auth/auth.module.ts), importing UsersModule:

// Filepath: .src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';

@Module({
  imports: [UsersModule],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Add Register logic using bcrypt

Now we are going to implement the register endpoint, we will use bcrypt to encrypt passwords:

npm install bcrypt

and

npm install --save-dev @types/bcrypt

Now, we need to add the logic to save the user to the database. But before that, we should create a DTO, I’ll name it “RegisterRequest” (register-request.ts). Since this DTO will be shared between auth and users module, I’ll create a new folder ‘shared’ and within that, a dto folder. Inside the dto folder, add a new class:

// Filepath: .src/shared/dto/register-request.ts
export class RegisterRequest {
  username: string;
  password: string;
  email: string;
}

Then, in UsersService we inject userRepository and add a register method which takes registerRequest as parameter. Inside the method, we create an User object from the properties of the DTO (registerRequest), but the password will be encrypted using bcrypt with an iteration of 10. Once the object is created, we call the repository to save the object into the database.

// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';

@Injectable()
export class UsersService {
  constructor(private readonly userRepository: UserRepository) {}

  async save(registerRequest: RegisterRequest): Promise<User> {
    const user = new User();
    user.email = registerRequest.email;
    user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
    user.username = registerRequest.username;
    return this.userRepository.save(user);
  }
}

Next, in AuthService, we should inject UsersService to call the save function:

// Filepath: .src/users/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(private readonly userService: UsersService) {}

  async register(registerRequest: RegisterRequest): Promise<string> {
    await this.userService.save(registerRequest);
    return 'we should replace this line to return the jwt';
  }
}

Then, let’s edit the AuthController. I prefer to use a route like /api/v{number}/auth instead of just /auth, so I’ll update it too:

// Filepath: .src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  async register(@Body() registerRequest: RegisterRequest): Promise<string> {
    return await this.authService.register(registerRequest);
  }
}

NOTE: In a real project, we should add validations to the DTO, such as ensuring it doesn’t allow null values, requiring a minimum password length, etc. We should also add validation in the entity, such as ensuring the email is unique, etc.

Add JWT

Next, let’s add the JWT dependency:

npm install --save @nestjs/jwt

Now, we should add the JWT configuration:

# Filepath: .env
DB_URL=localhost
DB_PORT=5432
DB_USER=username
DB_PASS=password
DB_NAME=databaseName

JWT_SECRET=00000000000000000000000000000000
JWT_EXPIRES=3600
// Filepath: .src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';
import { JwtModule } from '@nestjs/jwt';
import 'dotenv/config';

@Module({
  imports: [
    UsersModule,
    JwtModule.register({
      global: true,
      secret: process.env.JWT_SECRET,
      signOptions: {
        algorithm: 'HS256',
        expiresIn: process.env.JWT_EXPIRES,
      },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Then, we will modify AuthService.register to return the JWT after the user signs up:

// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async register(registerRequest: RegisterRequest): Promise<{ token: string }> {
    await this.userService.save(registerRequest);
    const payload = { sub: registerRequest.username };
    return {
      token: await this.jwtService.signAsync(payload),
    };
  }
}
// Filepath: .src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  async register(@Body() registerRequest: RegisterRequest): Promise<{ token: string }> {
    return await this.authService.register(registerRequest);
  }
}

Add Login logic

Now, we’re implementing the login method, So we need a LoginRequest DTO (with username and password), a findByUsername method in UserService, a login method in both AuthService and AuthController and a TokenResponse DTO:

// Filepath: .src/auth/dto/token-response.ts
export class TokenResponse {
  token: string;
}
// Filepath: .src/shared/dto/login-request.ts
export class LoginRequest {
  username: string;
  password: string;
}
// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';

@Injectable()
export class UsersService {
  constructor(private readonly userRepository: UserRepository) {}

  async save(registerRequest: RegisterRequest): Promise<User> {
    const user = new User();
    user.email = registerRequest.email;
    user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
    user.username = registerRequest.username;
    return this.userRepository.save(user);
  }

  async findByUsername(username: string): Promise<User | null> {
    return this.userRepository.findOne({
      where: { username: username },
    });
  }
}
// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginRequest } from '../shared/dto/login-request';
import * as bcrypt from 'bcrypt';
import { TokenResponse } from './dto/token-response';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async register(registerRequest: RegisterRequest): Promise<TokenResponse> {
    await this.userService.save(registerRequest);
    return this.createTokenFromUsername(registerRequest.username);
  }

  async login(loginRequest: LoginRequest): Promise<TokenResponse> {
    const user = await this.userService.findByUsername(loginRequest.username);
    if (!user) {
      throw new Error('Username or password invalid');
    }
    const passwordMatches = await bcrypt.compare(
      loginRequest.password,
      user.passwordHash,
    );
    if (!passwordMatches) {
      throw new Error('Username or password invalid');
    }
    return this.createTokenFromUsername(loginRequest.username);
  }

  private async createTokenFromUsername(
    username: string,
  ): Promise<TokenResponse> {
    const payload = { sub: username };
    const tokenResponse = new TokenResponse();
    tokenResponse.token = await this.jwtService.signAsync(payload);
    return tokenResponse;
  }
}
// Filepath: .src/auth/auth.controller.ts
import {
  Controller,
  Post,
  Body,
  HttpCode,
  ForbiddenException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
import { LoginRequest } from '../shared/dto/login-request';
import { TokenResponse } from './dto/token-response';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('register')
  async register(
    @Body() registerRequest: RegisterRequest,
  ): Promise<TokenResponse> {
    return await this.authService.register(registerRequest);
  }

  @HttpCode(200)
  @Post('login')
  async login(@Body() loginRequest: LoginRequest): Promise<TokenResponse> {
    try {
      return await this.authService.login(loginRequest);
    } catch (error) {
      throw new ForbiddenException();
    }
  }
}

Add Roles

First, we will configure the TypeOrmModule to allow us to use snakenaming strategy: npm i --save typeorm-naming-strategies

In app.module.ts :

// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';

@Module({
  imports: [
    AuthModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
      synchronize: true,
      autoLoadEntities: true,
      namingStrategy: new SnakeNamingStrategy(),
    }),
    UsersModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Now, we’ll add UserRole and Role entities, inside src/users/entities folder:

// Filepath: .src/users/entities/role.entity.ts
import {
  BaseEntity,
  Column,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { UserRole } from './user-role.entity';

@Entity()
export class Role extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  name: string;
  @Column({ nullable: true })
  deletedAt: Date;
  @OneToMany(() => UserRole, (userRole: UserRole): Role => userRole.role)
  userRoles: UserRole[];
}
// Filepath: .src/users/entities/user-role.entity.ts
import {
  BaseEntity,
  Column,
  Entity,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';
import { Role } from './role.entity';

@Entity('user_role')
export class UserRole extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  fromDate: Date;
  @Column({ nullable: true })
  toDate: Date;
  @ManyToOne(
    (): typeof User => User,
    (user: User): UserRole[] => user.userRoles,
  )
  user: User;
  @ManyToOne(() => Role, (role: Role): UserRole[] => role.userRoles, {
    eager: true,
  })
  role: Role;
}

Also we should update the User entity:

// Filepath: .src/users/entities/user.entity.ts
import {
  BaseEntity,
  Column,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import { UserRole } from './user-role.entity';

@Entity('users')
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  email: string;
  @Column()
  username: string;
  @Column()
  passwordHash: string;
  @OneToMany(
    (): typeof UserRole => UserRole,
    (role: UserRole): User => role.user,
    {
      eager: true,
      cascade: true,
    },
  )
  userRoles: UserRole[];
}

Add RoleRepository in users folder, it has a method to search for a Role entity by name:

// Filepath: .src/users/role.repository.ts
import { DataSource, Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { Role } from './entities/role.entity';

@Injectable()
export class RoleRepository extends Repository<Role> {
  constructor(dataSource: DataSource) {
    super(Role, dataSource.createEntityManager());
  }

  findByName(name: string): Promise<Role | null> {
    return this.findOne({ where: { name } });
  }
}

Update UsersModule:

// Filepath: .src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserRole } from './entities/user-role.entity';
import { Role } from './entities/role.entity';
import { RoleRepository } from './role.repository';

@Module({
  imports: [TypeOrmModule.forFeature([User, UserRole, Role])],
  providers: [UsersService, UserRepository, RoleRepository],
  exports: [UsersService],
})
export class UsersModule {}

Now we add the logic to assign the USER role when a user registers:

// Filepath: .src/shared/constants.ts
export const USER = 'USER';
export const ADMIN = 'ADMIN';
export const ROLE_PREFIX = 'ROLE_';
// Filepath: .src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import * as bcrypt from 'bcrypt';
import { RegisterRequest } from '../shared/dto/register-request';
import { Role } from './entities/role.entity';
import { UserRole } from './entities/user-role.entity';
import { RoleRepository } from './role.repository';
import { USER } from '../shared/constants';

@Injectable()
export class UsersService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly roleRepository: RoleRepository,
  ) {}

  async save(registerRequest: RegisterRequest): Promise<User> {
    let role: Role | null = await this.roleRepository.findByName(USER);
    if (!role) {
      role = new Role();
      role.name = USER;
      role = await this.roleRepository.save(role);
    }

    const userRole = new UserRole();
    userRole.fromDate = new Date();
    userRole.role = role;

    const user = new User();
    user.email = registerRequest.email;
    user.passwordHash = await bcrypt.hash(registerRequest.password, 10);
    user.username = registerRequest.username;
    user.userRoles = [userRole];
    return this.userRepository.save(user);
  }

  async findByUsername(username: string): Promise<User | null> {
    return this.userRepository.findOne({
      where: { username: username },
    });
  }
}

And finally we update AuthService.register to include the roles in the JWT payload:

// Filepath: .src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { RegisterRequest } from '../shared/dto/register-request';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { LoginRequest } from '../shared/dto/login-request';
import * as bcrypt from 'bcrypt';
import { TokenResponse } from './dto/token-response';
import { UserRole } from '../users/entities/user-role.entity';
import { User } from '../users/entities/user.entity';
import { ROLE_PREFIX } from '../shared/constants';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UsersService,
    private readonly jwtService: JwtService,
  ) {}

  async register(registerRequest: RegisterRequest): Promise<TokenResponse> {
    const user: User = await this.userService.save(registerRequest);
    return this.createTokenFromUsername(user.username, user.userRoles);
  }

  async login(loginRequest: LoginRequest): Promise<TokenResponse> {
    const user: User | null = await this.userService.findByUsername(
      loginRequest.username,
    );
    if (!user) {
      throw new Error('Username or password invalid');
    }
    const passwordMatches = await bcrypt.compare(
      loginRequest.password,
      user.passwordHash,
    );
    if (!passwordMatches) {
      throw new Error('Username or password invalid');
    }
    return this.createTokenFromUsername(user.username, user.userRoles);
  }

  private async createTokenFromUsername(
    username: string,
    userRoles: UserRole[],
  ): Promise<TokenResponse> {
    const payload = {
      sub: username,
      roles: userRoles.map(
        (ur: UserRole): string => ROLE_PREFIX + ur.role.name,
      ),
    };
    const tokenResponse = new TokenResponse();
    tokenResponse.token = await this.jwtService.signAsync(payload);
    return tokenResponse;
  }
}

Posts endpoint

Now, let’s start generating the Posts module and in it, PostsController and its DTOs:

nest g module posts           
nest g controller posts --no-spec

Create a dto folder and add PostRequest (post-request.ts) and PostResponse (post-response.ts)

// Filepath: .src/posts/dto/post-request.ts
export class PostRequest {
  userId: number;
  content: string;
}
// Filepath: .src/posts/dto/post-response.ts
export class PostRequest {
  userId: number;
  content: string;
}

Update PostsController, we’re keeping it simple, so we won’t implement the service, repository, etc., just the controller and its DTOs. The GET method will be allowed for authenticated users and the POST for ADMIN role.

// Filepath: .src/posts/posts.controller.ts
import { Body, Controller, Get, Post } from '@nestjs/common';
import { PostResponse } from './dto/post-response';
import { PostRequest } from './dto/post-request';

@Controller('api/v1/posts')
export class PostsController {
  posts: PostResponse[] = [
    { username: 'admin', content: 'entry 1' },
    { username: 'admin', content: 'entry 2' },
  ];

  @Get()
  getPosts(): PostResponse[] {
    return this.posts;
  }

  @Post()
  createPost(@Body() post: PostRequest): PostResponse[] {
    this.posts.push({ username: 'admin', content: post.content });
    return this.posts;
  }
}

Create AuthGuard

To do this, we need a Public decorator to allow request to be public, an AuthGuard to get the Authorization header, extract the JWT and add the user to the request.

First, create the Public decorator. Create a file at .src/auth named public.decorator.ts:

// Filepath: .src/auth/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Then, we’ll create the AuthGuard

nest g guard auth --no-spec

Update AuthGuard to extract token and add the user to the request:

// Filepath: .src/auth/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import 'dotenv/config';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from './public.decorator';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private reflector: Reflector,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const header = request.headers.authorization;
    if (header == null || !header.startsWith('Bearer ')) {
      throw new UnauthorizedException();
    }
    const token = header.split(' ')[1];
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });
      request['user'] = { username: payload.sub, roles: payload.roles };
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }
}

Update the AppModule to enable AuthGuard globally:

// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { PostsModule } from './posts/posts.module';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/auth.guard';

@Module({
  imports: [
    AuthModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
      synchronize: true,
      autoLoadEntities: true,
      namingStrategy: new SnakeNamingStrategy(),
    }),
    UsersModule,
    PostsModule,
  ],
  controllers: [],
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
  ],
})
export class AppModule {}

Next, update AuthController to make register and login public

// Filepath: .src/auth/auth.controller.ts
import {
  Controller,
  Post,
  Body,
  HttpCode,
  ForbiddenException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { RegisterRequest } from '../shared/dto/register-request';
import { LoginRequest } from '../shared/dto/login-request';
import { TokenResponse } from './dto/token-response';
import { Public } from './public.decorator';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Public()
  @Post('register')
  async register(
    @Body() registerRequest: RegisterRequest,
  ): Promise<TokenResponse> {
    return await this.authService.register(registerRequest);
  }

  @Public()
  @HttpCode(200)
  @Post('login')
  async login(@Body() loginRequest: LoginRequest): Promise<TokenResponse> {
    try {
      return await this.authService.login(loginRequest);
    } catch (error) {
      throw new ForbiddenException();
    }
  }
}

Restrict createPost to only allow admins

Now, let’s add a Role decorator:

// Filepath: .src/auth/role.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { ROLE_PREFIX } from '../shared/constants';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) =>
  SetMetadata(
    ROLES_KEY,
    roles.map((r) => ROLE_PREFIX + r),
  );

Create the RoleGuard:

// Filepath: .src/auth/role.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(
      ROLES_KEY,
      [context.getHandler(), context.getClass()],
    );
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

Then we add the RoleGuard to AppModule to enable it globally:

// Filepath: .src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
import 'dotenv/config';
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
import { PostsModule } from './posts/posts.module';
import { RolesGuard } from './auth/roles.guard';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth/auth.guard';

@Module({
  imports: [
    AuthModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
      username: process.env.DB_USER,
      password: process.env.DB_PASS,
      database: process.env.DB_NAME,
      synchronize: true,
      autoLoadEntities: true,
      namingStrategy: new SnakeNamingStrategy(),
    }),
    UsersModule,
    PostsModule,
  ],
  controllers: [],
  providers: [
    {
      provide: APP_GUARD,
      useClass: AuthGuard,
    },
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

Finally, update PostsController to allow creating a post only if the user is an admin:

// Filepath: .src/posts/posts.controller.ts
import { Body, Controller, Get, Post, Request } from '@nestjs/common';
import { PostResponse } from './dto/post-response';
import { PostRequest } from './dto/post-request';
import { Roles } from '../auth/roles.decorator';
import { ADMIN } from '../shared/constants';

@Controller('api/v1/posts')
export class PostsController {
  posts: PostResponse[] = [
    { username: 'admin', content: 'entry 1' },
    { username: 'admin', content: 'entry 2' },
  ];

  @Get()
  getPosts(): PostResponse[] {
    return this.posts;
  }

  @Roles(ADMIN)
  @Post()
  createPost(@Body() post: PostRequest, @Request() req: any): PostResponse[] {
    this.posts.push({ username: req.user.username, content: post.content });
    return this.posts;
  }
}

Comments: