NestJS API 04. Authenticating users with JWT (Bcrypt, Passport)


Authentication hay xác thực người dùng là thiết yếu trong hầu hết các ứng dụng. Có rất nhiều cách thể thực hiện điều này, nhưng trong serie lần này tìm hiểu về NestJS API nên chúng ra sẽ dùng token để xác thực. Cùng nhau bắt nay thực hiện ngay thôi nào.

Thư viện chúng ta sẽ tìm hiểu đó là passport, đây là thư viện xác thực của nodejs phổ biến nhất là mình từng biết. Đồng thời chúng ta cũng bảo mật tài khoản người dùng bằng cách mã khóa mật khẩu người dùng với thuật toán bcrypt

Theo dõi source code tại đây, branch dev nha

Link postman để test cho dễ.

Khởi tạo

Trước tiên, để mà xác thực được thì ta cần có 1 collection users, và chúng ta sẽ tách thằng user này ra thành 1 module riêng.

tương tự như bài trước, chỗ mình mình làm nhanh và sẽ không nói lại nữa nha:

nest g module user

Vào thư mục user, ta tạo liên tiếp các folder sau: config, controllers, dto, services, models, repositories. Rồi từ từ chúng ta sẽ tạo những file vào bên trong này, không bị dư thừa đâu các bạn yên tâm. Do lúc trước mình làm việc với laravel, nên thành ra các thư mục tạo ra khá giống với các tổ chức bên đó. Các bạn có thể theo hoặc không.

Vào folder  models tạo file user.model.ts

import { Schema, Document } from 'mongoose';

const UserSchema = new Schema(
  {
    name: String,
    email: String,
    password: String,
  },
  {
    collection: 'users',
  },
);

export { UserSchema };

export interface User extends Document {
  name: string;
  email: string;
  password: string;
}

Tiếp theo tạo file user.repository.ts bên trong folder ` repositories ``

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { BaseRepository } from '../../base.repository';
import { User } from '../models/user.model';

@Injectable()
export class UserRepository extends BaseRepository<User> {
  constructor(
    @InjectModel('User')
    private readonly userModel: Model<User>,
  ) {
    super(userModel);
  }
}

Đến lượt thằng service, tạo file user.service.ts bên trong folder services:

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { UserRepository } from '../repositories/user.repository';
import { CreateUserDto, LoginUserDto } from '../dto/user.dto';
import * as bcrypt from 'bcrypt';
import e from 'express';

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

  async create(userDto: CreateUserDto) {
    userDto.password = await bcrypt.hash(userDto.password, 10);

    // check exists
    const userInDb = await this.userRepository.findByCondition({
      email: userDto.email,
    });
    if (userInDb) {
      throw new HttpException('User already exists', HttpStatus.BAD_REQUEST);
    }

    return await this.userRepository.create(userDto);
  }

  async findByLogin({ email, password }: LoginUserDto) {
    const user = await this.userRepository.findByCondition({
      email: email,
    });

    if (!user) {
      throw new HttpException('User not found', HttpStatus.UNAUTHORIZED);
    }

    const is_equal = bcrypt.compareSync(password, user.password);

    if (!is_equal) {
      throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED);
    }

    return user;
  }
}

Ở trong service mình có gọi thằng dto ra, vậy nên ta tạo thêm thằng user.dto.ts bên trong folder dto.

import { IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty() email: string;
  @IsNotEmpty() name: string;
  @IsNotEmpty() password: string;
}

export class LoginUserDto {
  @IsNotEmpty() email: string;
  @IsNotEmpty() password: string;
}

Các bạn để ý cách đặt tên file của mình có gì đặt biết không? Nếu các bạn thấy hay thì có thể làm theo hoặc đặt theo ý của bạn, tùy thích.

Tới đây vẫn chưa chạy được đâu, với tới đoạn đăng ký tài khoảo à, còn chưa có controller nữa. Tạm thời cứ xử lý thằng service này đã.

Xử lý

Đầu bài mình cũng có nhắc đến chỗ này, mình phải mã hóa mật khẩu trước khi lưu nó vào csdl. Và thuật toán chúng ta dùng là Bcrypt.

Cài đặt thư viện

npm install @types/bcrypt bcrypt

Khi chúng ta sử dụng bcrypt, chúng ta định nghĩa salt rounds. Số này càng lớn thì càng khó để đảo ngược hàm băm với brute-forcing. Thông thường giá trị 10 là đủ.

const passwordInPlaintext = '12345678';
const hash = await bcrypt.hash(passwordInPlaintext, 10);
 
const isPasswordMatching = await bcrypt.compare(passwordInPlaintext, hashedPassword);
console.log(isPasswordMatching); // true

Bây giờ ta tạo auth.service.ts bên trong folder services để xử lý những công việc liên quan đến authen.

Cài một số thư viện cần thiết nha

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

npm install --save @nestjs/passport passport

auth.service.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, LoginUserDto } from '../dto/user.dto';
import { JwtService } from '@nestjs/jwt';

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

  async register(userDto: CreateUserDto) {
    const user = await this.userService.create(userDto);

    const token = this._createToken(user);
    return {
      email: user.email,
      ...token,
    };
  }

  async login(loginUserDto: LoginUserDto) {
    const user = await this.userService.findByLogin(loginUserDto);
    const token = this._createToken(user);

    return {
      email: user.email,
      ...token,
    };
  }

  async validateUser(email) {
    const user = await this.userService.findByEmail(email);
    if (!user) {
      throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
    }
    return user;
  }

  private _createToken({ email }): any {
    const accessToken = this.jwtService.sign({ email });
    return {
      expiresIn: process.env.EXPIRESIN,
      accessToken,
    };
  }
}

Và chúng ta cần nơi để tiếp nhận và xử lý các request đó chính là controller. Tạo file auth.controller.ts bên trong folder controller

import {
  Body,
  Controller,
  HttpException,
  HttpStatus,
  Post,
} from '@nestjs/common';
import { CreateUserDto, LoginUserDto } from 'src/user/dto/user.dto';
import { AuthService } from '../services/auth.service';

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

  @Post('register')
  async register(@Body() createUserDto: CreateUserDto) {
    return await this.authService.register(createUserDto);
  }

  @Post('login')
  async login(@Body() loginUserDto: LoginUserDto) {
    return await this.authService.login(loginUserDto);
  }
}

Tạm thời chỗ này mình chỉ khai báo 2 controller dành cho việc đăng ký và đăng nhập, còn những chức năng râu ria khác mình sẽ làm ở những bài sau.

Sau khi đăng ký hoặc đăng nhập, ta đã có cái token đó. Vậy bây giờ làm sao để chuyển hóa cái token đó trở lại thành cái email mà ta đã truyền vào. Đơn giản, ta chỉ cần tạo file jwt.strategy.ts cùng cấp với thằng user.module.ts

import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './services/auth.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.SECRETKEY,
    });
  }

  async validate({ email }) {
    const user = await this.authService.validateUser(email);

    if (!user) {
      throw new HttpException('Invalid token', HttpStatus.UNAUTHORIZED);
    }

    return user;
  }
}

Bây giờ mình sẽ nạp hết những thứ mình đã khai báo phía trên vào UserModule

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './models/user.model';
import { UserRepository } from './repositories/user.repository';
import { UserService } from './services/user.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './controllers/auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { AuthService } from './services/auth.service';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: 'User',
        schema: UserSchema,
      },
    ]),
    PassportModule.register({
      defaultStrategy: 'jwt',
      property: 'user',
      session: false,
    }),
    JwtModule.register({
      secret: process.env.SECRETKEY,
      signOptions: {
        expiresIn: process.env.EXPIRESIN,
      },
    }),
  ],
  controllers: [AuthController],
  providers: [UserRepository, UserService, JwtStrategy, AuthService],
})
export class UserModule {}

Tới đây ta chỉ có thể sử dụng để đăng ký, đăng nhập. Còn việc xác thực đăng nhập, ta có thể thông qua thằng @UseGuards(AuthGuard()).

Tạo thằng user.controller.ts và tạo ở đâu chắc các bạn cũng biết rồi nhỉ, trong folder controllers. Và nhớ nạp nó vào trong thằng UserModule luôn nha

import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('user')
export class UserController {
  @UseGuards(AuthGuard())
  @Get('profile')
  async getProfile(@Req() req: any) {
    return req.user;
  }
}

UserModule

...
  controllers: [AuthController, UserController],
...

Bật ngay PostMan lên và test api ngay thôi nào.

Kết luận

Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC