Skip to main content

Authentication & Role-Based Access Control Using Passport & JWT in NestJs MYSQL

In the earlier tutorials, you learnt to create API endpoints to access MYSQL database and upload files in NestJs. In a real world application, it is common to protect API endpoints by restricting the APIs and grant permissions to only authenticated users. 

From a client app, a visitor can register a new account using username, email, password. The default role of the visitor is user. The password is encrypted using bcrypt package. Then after successful registration, he/she can login using username and password. Upon successful login, a valid JWT (JSON Web Token) will be generated and returned to the client. To get permission to access subsequent API endpoints, the client has to send the token in headers to be validated on server.

The very popular package to do authentication in NestJs is Passport. In the nestjs app, execute the following commands to install dependencies:

npm install --save @nestjs/passport passport passport-local

npm install --save-dev @types/passport-local

npm install --save @nestjs/jwt passport-jwt

npm install --save-dev @types/passport-jwt

npm install --save bcrypt

npm install --save-dev @types/bcrypt


The @nestjs/jwt package is a utility package that helps generating and validating JWT . The passport-jwt package is the Passport package that implements the JWT strategy and @types/passport-jwt  provides the TypeScript type definitions.

In the entities folder, create User and Role entities. One user may have many roles. A role may belong to many users. Also, One user may have many products.

entities/user.entity.ts

import { Entity, Column, PrimaryGeneratedColumn,
OneToMany, JoinColumn }
from 'typeorm';
import { Product } from './product.entity';
import { Role } from './role.entity';
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 50, default: '' })
  firstName: string;

  @Column({ type: 'varchar', length: 50, default: '' })
  lastName: string;

  @Column({ type: 'varchar', length: 100, default: '' })
  userName: string;

  @Column()
  email: string;
 
  @Column()
  password: string;
 
  @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
  created_ad: Date;

  @Column({ default: true })
  isActive: boolean;

// make one to many relationship to product table
  @OneToMany(type => Product, product => product.user)
  products: Product[];

// make on to many relationship to role table
  @OneToMany(type => Role, role => role.user,{
    cascade: true,
    onDelete: 'CASCADE',
    onUpdate:'CASCADE'
  })
  roles: Role[];

}

entities/role.entitiy.ts

import { Entity, Column,
PrimaryGeneratedColumn, ManyToOne
}
from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

// make many to on relationship to user table
  @ManyToOne(() => User, (user) => user.roles)
  user: User

 
}

Modify the App module to include User & Role entities. 

.............
TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'user',
      password: 'password',
      database: 'database',
      entities: [ Product, Role, User],
      synchronize: true,
    }),
...........

While MYSQL is running, save the project. The user and role tables will be created in the database.

Run the following commands to create auth module and service, users module, service, and controller:

npx nest g module auth

npx nest g service auth

npx nest g module users

npx nest g service users

npx nest g controller users


We need to implement Passport strategy to use Passport in the authentication. In the src/auth folder, create local.strategy.ts file. The local strategy - LocalStrategy needs to extend PassportStrategy class to override the validate function to validate a user by username and password with the help of validateUser function from AuthService. The LocalStrategy class will be used by AuthGard to initialize authentication flow. In the login process, AuthGard invokes the local strategy to validate the user. If validation is successful, the user data (req.user) is passed to the login function that handles login route. The login function (defined in AuthService) uses the user data to generate a token to authenticate the user. 

auth/local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }

  async validate(userName: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(userName, password);
    return user;
  }
}

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

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

src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from 'src/entities/user.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
    constructor(
        @InjectRepository(User)
        private usersRepository: Repository<User>,
      ) {}
      // list all users from user table
      findAll(): Promise<User[]> {
      
        return this.usersRepository.find(
          {
            
            // include rows from role table
            relations:
              ['roles']
            
          }
        );
      }
      // get user by id
      findOne(id: number): Promise<User> {
        return this.usersRepository.findOneBy({ id });
      }
    
      // create a new user 
      async create(user: User): Promise<User>{
        //hashing password
        user.password=await bcrypt.hash(user.password, 10);
        //save user in database
        const result=await this.usersRepository.save(user);
        return result;
      

      } 

      // get user by username
      async findOneByUserName(username: string): Promise<User | undefined> {
        return this.usersRepository.findOne({
          where:{userName:username},
          relations:["roles"]
        }
        );
      }
}

In the UsersService, we define convenient functions to list all users, get user by id, by username, and create a new user in the database with the help of UsersRepository. 

users/users.controller.ts
import { Body, Controller, Get, Param, Post, Put, Res, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { User } from 'src/entities/user.entity';
import { UsersService } from './users.service';

@Controller('users')

export class UserController {
    constructor(private userService: UsersService) {}

    @Post('register')
    async create(@Body() user: User,@Res() res) {
        console.log("user submit=",user);
        const result=await this.userService.create(user);
        res.send({result});
        
    }
   

    @Get('all')
    async findAllAsync(): Promise<User[]> {
        return this.userService.findAll();
    }
}
In the UsersController, simply define register function to handle registration route to save a new user and list all  users from user table of the database.

auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

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

    async validateUser(username: string, pass: string): Promise<any> {
      const user = await this.usersService.findOneByUserName(username);
      const isCorrectPassword = user && await bcrypt.compare(
        pass,
        user.password
      );

      if (isCorrectPassword) {

        const { password, ...result } = user; // exclude password from result
        return result; // return user data
      }
      return({err:"invalid user"}); // return error message
    }

    // generate token
    async login(user: any) {
        const payload = { username: user.userName, email:user.email, sub: user.id, roles:user.roles };
        return {
          token: this.jwtService.sign(payload),
        };
      }
}

In the AuthService, the validateUser function validates a user using username and password with the help of findOneByUserName function of UsersService and compare function of bcrypt. The login function is invoked by the login route after the username and password are validated.

auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';

@Module({
    imports: [UsersModule, PassportModule,
        JwtModule.register({
            secret: jwtConstants.secret,
            signOptions: { expiresIn: '6000s' },
          }),
    ],
    providers: [AuthService, LocalStrategy],

    exports: [AuthService],
})
export class AuthModule {}

In the AuthModule class, we configure JWT secret key and expiration using register function of JwtModule. To allow AuthGard to use the local strategy, you need to add the LocalStrategy class to the providers list of AuthModule.

auth/constants.ts
export const jwtConstants = {
    secret: 'JwtsecretKey',
  };

Update the app.controller.ts file to handle user login.

src/app.controller.ts
import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller()
export class AppController {
  constructor(private authService: AuthService) {}
  @UseGuards(AuthGuard('local')) // verify username and password
  @Post('auth/login') // then pass the user data to login
  async login(@Request() req) {
 
    if(req.user.err) return req.user.err; // return error message to client
    else return this.authService.login(req.user); // return token to client
   
  }
 

}

Save the project. Now our register and login API endpoints are ready to test. Open ARC chrome extension and try to create a new user using http://localhost:3000/users/register. 



Then, login via http://localhost:3000/auth/login Url







To list all registered users, select method GET and input Request Url: http://localhost:3000/users/all.

Protect API endpoints

Passport provides the passport-jwt strategy for securing RESTful API endpoints with JSON Web Token. In the auth folder crate jwt-strategy.ts. The jwt strategy extracts token from request headers and verify its signature. Then it calls the validate() method to pass the user data to a next method that handles the protected API.

auth/jwt-strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }

  async validate(payload: any) {

    return { userId: payload.sub, username: payload.username, roles:payload.roles };
  }
}

Update auth.module.ts file to include JwtStrategy in the providers list:
......
providers: [AuthService, LocalStrategy,JwtStrategy],
......

Now we can use AuthGuard('jwt') to protect our routes. Open users/users.controller.ts to Jwt AuthGuard.
@UseGuards(AuthGuard('jwt'))
    @Get('all')
    async findAllAsync(): Promise<User[]> {
        return this.userService.findAll();
   } 

Try access  http://localhost:3000/users/all again. Without passing a valid token, you will get "Unauthorized" error message. To be authorized, you need to log in. Then add Authentication header to the request.


Role-Based Access Control

Currently  the users/all route can be accessed by all authenticated users. However, we need to allow only authenticated users that have admin role to list all users from the user table. How to achieve this goal? First, we need to use @SetMetadata decorator to define role to access the route.
    
@UseGuards(AuthGuard('jwt'))
    @SetMetadata('roles', ['admin'])
    @Get('all')
    async findAllAsync(): Promise<User[]> {
        return this.userService.findAll();
    } 
Then, we use a role guard to compare the role specified in the route with the user role from the request. In src/guards, create roles.guards.ts file.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';


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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles',
        context.getHandler()
    );

    if (!roles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    const user_roles= [];
    user.roles.map(role =>{
        user_roles.push(role.title);
    });
    return roles.some((role) => user_roles?.includes(role));
  }
}

Add the RoleGuard to the route:
    @UseGuards(AuthGuard('jwt'),RolesGuard)
    @SetMetadata('roles', ['admin'])
    @Get('all')
    async findAllAsync(): Promise<User[]> {
        return this.userService.findAll();
    }
If you try to access http://localhost:3000/users/all with passing a valid token, you will get the following error:
{
"statusCode": 403
"message": "Forbidden resource"
"error": "Forbidden"
}

This is because the authorized user created previously does not have admin role.
To complete this tutorial, update products.controller file, to protect add, update, and delete routes.
.............................
import { UseInterceptors,ParseFilePipe, FileTypeValidator,
   MaxFileSizeValidator, UploadedFile} from '@nestjs/common';
import { Express } from 'express';
import { diskStorage } from 'multer';
import * as path from 'path';
import { Product } from 'src/entities/product.entity';
import { RolesGuard } from 'src/guards/roles.guard';
import { ProductserviceService } from 'src/products/productservice.service';

// local storage configuration
var storage = diskStorage({
  destination: function (req, file, callback) {
    callback(null, 'public/uploads');
  },
  filename: function (req, file, callback) {
      callback(null, file.fieldname + '-' + Date.now()+ path.extname(file.originalname));
   
  }
});
// image file validation
var fileUploadValidator= new ParseFilePipe({
  validators: [
    new MaxFileSizeValidator({ maxSize: 200000 }),
    new FileTypeValidator({fileType:  new RegExp('.(jpg|jpeg|png|gif)$')}),

  ],
});

// route to upload form data to insert product data to database
    @UseGuards(AuthGuard('jwt'),RolesGuard)
    @SetMetadata('roles', ['admin']
    @Post('add')
    @UseInterceptors(FileInterceptor('image',{
        storage: storage,
       
    }))
    async uploadFile(@Req() req, @Res() res,@UploadedFile(
      fileUploadValidator,
    ) file: Express.Multer.File) {
      const {body} = req;
      const {userId} = req.user; // get user id from request
      if(body && userId){
       
        let fname='';
        if(file) fname=file.filename;
        const pro=new Product();
        pro.name=body.name;
        pro.description=body.description;
        pro.price=body.price;
        pro.thumbnail=fname;
        pro.created_at=new Date();
        pro.updated_at=new Date();
        pro.user=userId;
       

        await this.productsService.create(pro);
        res.send({
          message: 'success',
          data:pro
        });

      }
      else{
        res.send({
          message: 'fail',
          data:{}
        });
      }
     
    }

 // route to upload form data to update product data in database   
    @UseInterceptors(FileInterceptor('image',{
      storage: storage,
    }))
    @UseGuards(AuthGuard('jwt'),RolesGuard)
    @SetMetadata('roles', ['admin']
    @Put(':id')
    async update(@Req() req, @Param('id') id: number, @Body() product: Product,
@Res() res, @UploadedFile(fileUploadValidator) file: Express.Multer.File) {
      product.updated_at=new Date();
      let fname='';
      if(file) fname=file.filename;
      if(fname!=='') product.thumbnail=fname;
      const result=await this.productsService.update(id,product);
      if(result){
        res.send({message:'success'});
      }
      else{
        res.send({message:'fail'});
      }
    }

    @UseGuards(AuthGuard('jwt'),RolesGuard)
    @SetMetadata('roles', ['admin']
    @Delete('/delete/:id')
    remove(@Param('id') id: string) {
      this.productsService.remove(id);
    }
...................

Here are Angular tutorials to build a client app to access APIs from server.

Comments

Popular posts from this blog

Filter and count rows in Nestjs & MYSQL database

In the earlier tutorial, you learnt to connect NestJs app with MYSQL database and do migration to add new column to the product table. Now we move on creating APIs to filter data by name with counting the number of products, by id, and delete a specific product from the database.  Execute the following commands to create products module, controller, and service in products folder: npx nest g module products npx nest g controller products npx nest g service products Update products/products.module.ts file to specify the products repository used in the current scope using forFeature method.  import { TypeOrmModule } from '@nestjs/typeorm' ; import { Product } from 'src/entities/product.entity' ; import { ProductsController } from './products.controller' ; import { ProductsService } from './product.service' ; @ Module ({   imports: [ TypeOrmModule . forFeature ([ Product ])],   controllers: [ ProductsController ],   providers: [ ProductServi

Upload form data with image file in NestJs & MYSQL

 In the previous tutorial, you learned how to filter and count rows in NestJs with TypeORM & MYSQL database . This tutorial teaches you how to upload form data in NestJs & MYSQL. We will create two more routes to insert and update product data. A client app is able to submit form data with an image file. The max image file size in byte is 200000 and it must be jpg, jpeg, png, or gif type.  To upload files in NestJs, it is required to install Multer typings package. npm install @types/multer Create public/uploads folder in the nestjs project to store uploaded files. Then, update the products/ProductsController.tsx file to add the following code: ............................. import { UseInterceptors, ParseFilePipe , FileTypeValidator ,     MaxFileSizeValidator , UploadedFile } from '@nestjs/common' ; import { Express } from 'express' ; import { diskStorage } from 'multer' ; import * as path from 'path' ; import { Product } from &#