Tutorial REST API dengan NestJS + MongoDB Part 2 — JWT & Authentication

Implementasi JWT pada NestJS untuk mengamankan routes API milikmu!

Hudya
9 min readApr 18, 2021

Halo semua, Kiddy disini dan pada kali ini kita mau ngulik lanjutan dari NestJS di tutorial sebelumnya kita sudah berhasil membuat CRUD dengan NestJS, untuk tutorialnya klik di bawah ini.

Nah pada tutorial kali ini, kita akan melanjutkan untuk belajar caranya membuat API lebih secure dengan implementasi JWT. Sebelumnya saya anggap kamu sudah paham ya apa itu JWT sehingga saya tidak perlu menjelaskannya lebih detail. Oke tanpa basa-basi kita cus langsung coding ajah. Nah untuk repo-nya saya menggunakan repo saya yang kemarin ya!

Di tutorial ini kita akan mengimplementasikan JWT, dengan konsep sebagai berikut:

  1. UserModule akan berfungsi sebagai modul user dimana kita akan melakukan logic saat registrasi, maupun pencarian user by email untuk login.
  2. AuthModule akan berfungsi sebagai modul autentikasi dimana logic registrasi akan mem-forward ke user service, dan untuk login kita hanya mengambil fungsi pencarain user by email dan logic untuk check hash-nya kita lakukan di modul autentikasi, tujuannya agar membuat konsep single responsibility, sehingga modul user kamu tidak jelek secara penulisan.

Oke tanpa basa-basi kita langsung mulai saja. Pertama kita instalasi dulu si bcrypt librarynya dari si NestJS.

npm i bcrypt --save
npm i -D @types/bcrypt --save-dev

Setelahnya kita langsung mulai generate dua resource alias dua modul.

nest g resource user
nest g resource auth

Seperti yang saya jelaskan di atas, user akan berfokus kepada user logic saja, dan auth akan berfokus kepada auth logic saja.

Seperti biasa nih kita koding dulu bagian usernya, pertama kita baut dulu schema untuk MongoDBnya. Buat folder schemas pada user dan buat file user.schema.ts, lalu paste kode berikut:

import * as mongoose from 'mongoose';export const UserSchema = new mongoose.Schema({
name: { type: String },
email: { type: String, index: true },
password: { type: String },
});

Sekarang kita buat folder interfaces dan buat user.interface.ts, lalu salin kode berikut.

import { Document } from 'mongoose';export class User extends Document {
readonly name: string;
readonly email: String;
password: string;
}

Nah untuk password kita tidak membuatnya sebagai readonly, kenapa begitu? Soalnya nanti kita mau tiban si password, sehingga kita tidak boleh membuatnya sebagai object observe saja, melainkan harus bisa ditimpa.

Nah seperti biasa kita extend dulu si CreateUserDto pada folder dto agar mengextend si interface user.

import { User } from "../interfaces/user.interface";export class CreateUserDto extends User {}

Nah kalau sudah berarti kita bisa membuat transformernya lebih dahulu, caranya buat folder transformers di folder user, dan buat file user.transformer.ts.

import { BaseTransformer } from "src/transformer.base"export class UserTransformer extends BaseTransformer {
static singleTransform (element) {
return {
id: element.id,
name: element.name,
email: element.email
}
}
}

Kalau sudah saatnya kita oprek habis-habisan user.service.ts agar menjadi sebagai berikut:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './interfaces/user.interface';
import { UserTransformer } from './transformers/user.transformer';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UserService {
constructor(@InjectModel('User') private UserModel: Model<User>) { }async create(createUserDto: CreateUserDto): Promise<UserTransformer> {
let data = new this.UserModel(createUserDto)

const saltOrRounds = 10; // Faktor penentu hashing, semakin tinggi semakin baik, tapi makan waktu hashing, 10 sudah cukup
const hash = await bcrypt.hash(data.password, saltOrRounds); // Kita hash password yang dijadikan objek dari dto
data.password = hash // Password yang ada di objek kita timpa dengan password yang sudah dihash
return UserTransformer.singleTransform(await data.save())
}
findAll() {
return `This action returns all user`;
}
findOne(id: number) {
return `This action returns a #${id} user`;
}
async findOneByEmail(email: string): Promise<UserTransformer> {
let data = await this.UserModel.findOne({ email: email })
if (!data) {
throw new Error('Data not found!')
}
return UserTransformer.singleTransform(data)
}
async findOneByEmailObject(email: string): Promise<User> {
let data = await this.UserModel.findOne({ email: email })
if (!data) {
throw new Error('User not found!')
}
return data
}
update(id: number, updateUserDto: UpdateUserDto) {
return `This action updates a #${id} user`;
}
remove(id: number) {
return `This action removes a #${id} user`;
}
}

Oke mantap, sekarang user service kita sudah siap nih, namun mungkin ada sedikit pertanyaan, biar saya tebak.

“Bang saltOrRounds apaan tuh?”

With “salt round” they actually mean the cost factor. The cost factor controls how much time is needed to calculate a single BCrypt hash. The higher the cost factor, the more hashing rounds are done. Increasing the cost factor by 1 doubles the necessary time. The more time is necessary, the more difficult is brute-forcing.

Sumber: https://stackoverflow.com/questions/46693430/what-are-salt-rounds-and-how-are-salts-stored-in-bcrypt

Yang artinya, dengan adanya salt round itu berarti cost dari si factor hashing, dimana kalo makin besar maka hashingnya semakin sulit, dan tentunya saja takes time juga saat melakukan hash.

“Bang apa bedanya findOneByEmail dan findOneByEmailObject?”

Hmm sebenernya ini udah jelas banget sih bedanya, kalo yang satu mengembalikan transformer yang satu mengembalikan object interface user, sudah dapat diliat dari expectation return si fungsi(): Promise<tipe>.

Sekarang masuk ke user.module.ts lalu jangan lupa import si mongoose schemanya.

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { MongooseModule } from '@nestjs/mongoose';
import { UserSchema } from './schemas/user.schema';
@Module({
imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])],
controllers: [UserController],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}

Kita juga menambahkan UserService kedalam exports, tujuannya agar nanti UserService dapat digunakan di modul Auth.

Sekarang kita pindah ke modul auth, tapi sebelumnya kita coba install dulu NestJS config, agar kita dapat menggunakan .env pada projek kita.

npm i --save @nestjs/config

Sekarang pada root folder kita buat file .env

Isi saja .env kamu dengan data berikut.

JWT_SECRET=your-secret-key

Sekarang kita pergi ke auth.service.ts dan kita oprek isinya.

import { Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import * as bcrypt from 'bcrypt';
import { UserTransformer } from 'src/user/transformers/user.transformer';
import { JwtService } from '@nestjs/jwt';
import { UserService } from 'src/user/user.service';
@Injectable()
export class AuthService {
constructor(
private userService: UserService,
private jwtService: JwtService
) { }
async login(createAuthDto: CreateAuthDto) {
let data = await this.userService.findOneByEmailObject(createAuthDto.email)
if (!data || !await bcrypt.compare(createAuthDto.password, data.password)) {
throw new Error('Invalid username or password')
}
let transformer = UserTransformer.singleTransform(data)
transformer['access_token'] = this.jwtService.sign({ "id": transformer['id'] }, { secret: process.env.JWT_SECRET })
return transformer
}
}

Setelahnya kita oprek auth.controller.ts

import { Controller, Get, Post, Body, Patch, Param, Delete, Res } from '@nestjs/common';
import { UserService } from 'src/user/user.service';
import { AuthService } from './auth.service';
import { CreateAuthDto } from './dto/create-auth.dto';
import { CreateUserDto } from '../user/dto/create-user.dto';
import { AppResponse } from 'src/response.base';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userService: UserService
) {}
@Post('login')
async login(@Res() res, @Body() createAuthDto: CreateAuthDto) {
try {
let data = await this.authService.login(createAuthDto);
return AppResponse.ok(res, data, "Success!")
} catch (e) {
console.trace(e)
return AppResponse.badRequest(res, "", e.message)
}
}
@Post('register')
async register(@Res() res, @Body() createUserDto: CreateUserDto) {
try {
let data = await this.userService.create(createUserDto);
return AppResponse.ok(res, data, "Successfully register, now you could login!")
} catch (e) {
return AppResponse.badRequest(res, "", e.message)
}
}
}

Bisa dilihat disini kan bahwa kita memanggil user service pada auth service, sehingga kita perlu melakukan import user module, karena kalau tidak kita import akan ada error dari NestJS itu sendiri diakibatkan User Service bukan merupakan bagian dari auth.

Tapi tunggu dulu, kita siapkan strategy alias sebuah file yang berfungsi untuk memvalidasi token kita.

Sebelumnya kita install terlebih dahulu yang kita perlukan, ada banyak nih hahahaha.

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 passport-jwt

NestJS support Passport dalam membuat autentikasinya, hal ini juga udah dipermudah oleh NestJS karena NestJS kan memang Highly opinionated framework, sehingga kamu ngga perlu bikin-bikin fungsi JWT-nya lagi kecuali kamu perlu custom lebih dalam.

Nah sekarang buat sebuah file jwt.strategy.ts didalam folder auth dan masukkan kode berikut.

import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}

Fungsi dari JWT Strategy ini adalah memverifikasi tokenmu apakah sudah benar atau belum. Sekarang kita butuh sebuah Guard, alias semacam file autentikasi setiap sebuah route diakses. Buat guard bisa dengan cara manual, atau dengan CLI NestJS.

nest g guard jwt-auth

Kalau kamu menggunakan CLI NestJS hasil dari jwt-auth.guard.ts dan jwt-auth.guard.spec.ts akan berada pada folder root, pindahkan kedua file tsb kedalam folder auth.

Sebelumnya kita buat dulu, karena kita ingin mengoprek kembalian exception dari Unauthorized exception agar tidak mengikuti format NestJS.

{
"statusCode": 500,
"message": "Internal server error"
}

Nah karena base key json kita kemarin adalah values, dan message, maka kita perlu juga membuat base seperti itu, tujuannya ya mempermudah frontend agar tidak salah parsing.

Buat folder exception sejajar dengan todo dan auth, lalu buat file unauthorized.exception.ts didalamnya. Berikut gambarannya.

Nah sekarang tempelkan kode berikut:

import { HttpException, HttpStatus } from "@nestjs/common";export class UnauthorizedException extends HttpException {
constructor(message: string = "Unauthorized!") {
super({ values: "", message: message }, HttpStatus.UNAUTHORIZED);
}
}

Jadi kita akan membuat base function dari si Unauthorized Exception yang dimana ini digunakan pada file guard yang akan kita buat.

Sekarang ubah isi jwt-auth.guard.ts dengan kode berikut.

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UnauthorizedException } from '../exception/unauthorized.exception'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Add your custom authentication logic here
// for example, call super.logIn(request) to establish a session.
return super.canActivate(context);
}

handleRequest(err, user, info) {
// You can throw an exception based on either "info" or "err" arguments
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}

Nah sekarang kita udah bisa nyoba nih, yuk nyobain dulu biar bisa lihat hasilnya.

npm run start:dev

Sekarang kita coba akses ke routes register.

Mantap sudah bisa, dan sekarang bisa kita test ke URL Login.

Loh tapi gimana caranya nih memastikan bahwa URL todo kita sudah berhasil dijaga dengan JWT? Soalnya saya akses todo tanpa token pun masih bisa. Tenang, Caranya pergi ke todo.controller.ts, kita akan mengubah salah satu fungsi saja untuk percobaan, yaitu findAll.

@UseGuards(JwtAuthGuard)
@Get()
async findAll(@Res() res) {
try {
let data = await this.todoService.findAll();
return AppResponse.ok(res, data)
} catch (e) {
return AppResponse.badRequest(res, "", e.message)
}
}

Jangan lupa untuk import filenya di bagian atas.

import { Controller, Get, Post, Body, Param, Delete, Res, Put, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';

Sekarang kita coba akses route get all todo tanpa token.

Apabila kita menggunakan token

Magnificent! Kita sudah berhasil mengimplementasikan JWT dan memasang penjaganya untuk salah satu Route URL kita.

Mudah kan mengimplementasikan JWT pada NestJS. NestJS membuat segalanya menjadi lebih mudah dan lebih sederhana, abstraksi-abstraksi fungsi yang dilakukan oleh NestJS membuat kita sebagai developer semakin produktif dan tidak bertele-tele menghabiskan waktu untuk membuat sebuah base fungsi keamanan API kita, berbeda dengan Express JS atau Fastify JS yang merupakan unopinonated library sehingga kita banyak harus membuat sebuah fungsi base-nya sendiri dan tentu saja standar fungsi akan berbeda-beda tiap developer.

Mungkin itu saja yang dapat saya sampaikan, pada tutorial selanjutnya kita akan mengimplementasikan Todo dengan User, jadi stay tune ya guys!

Sampai jumpa di tutorial selanjutnya dan happy coding!

--

--

Hudya
Hudya

Written by Hudya

Which is more difficult, coding or counting? Not both of them, the difficult one is sharing your knowledge to people without asking the payment.

No responses yet