Mini Project: JWT Auth + Role-Based Guard di NestJS

πŸ”‘ Intro

Pernah bingung bedanya login sama izin akses? πŸ€”
Login = siapa lo (auth), izin akses = boleh ngapain aja (role).

Di NestJS, kita bisa handle itu pake JWT buat login dan Role-Based Guard buat nentuin siapa boleh ngakses apa. 🎯

Di mini project ini, kita bakal bikin step by step: login dapet token JWT, terus pake role guard biar cuma role tertentu yang bisa masuk ke endpoint tertentu. Simple, clean, dan gampang dipraktekkin. πŸš€

1️⃣ Install Package Dulu

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

2️⃣ Struktur Folder Sederhana

src/
 ┣ auth/
 ┃ ┣ auth.module.ts
 ┃ ┣ auth.service.ts
 ┃ ┣ auth.controller.ts
 ┃ ┣ jwt.strategy.ts
 ┃ ┣ roles.decorator.ts
 ┃ β”— roles.guard.ts
 ┣ users/
 ┃ β”— users.service.ts
 ┣ app.module.ts
 β”— main.ts

3️⃣ Users Service (Dummy Data)

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  private users = [
    { id: 1, username: 'alice', password: '1234', role: 'user' },
    { id: 2, username: 'bob', password: '1234', role: 'admin' },
  ];

  async findOne(username: string) {
    return this.users.find(user => user.username === username);
  }
}

4️⃣ Auth Service

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

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

  async validateUser(username: string, pass: string) {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    if (!user) throw new UnauthorizedException('Invalid credentials');

    const payload = { username: user.username, sub: user.id, role: user.role };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

5️⃣ Auth Controller

// src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';

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

  @Post('login')
  async login(@Body() body: { username: string; password: string }) {
    const user = await this.authService.validateUser(body.username, body.password);
    return this.authService.login(user);
  }
}

6️⃣ JWT Strategy

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || 'superSecretKey',
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username, role: payload.role };
  }
}

7️⃣ Roles Decorator + Guard

// src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// src/auth/roles.guard.ts
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 requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}

8️⃣ Auth Module

// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { UsersService } from '../users/users.service';

@Module({
  imports: [
    PassportModule,
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'superSecretKey',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  providers: [AuthService, JwtStrategy, UsersService],
  controllers: [AuthController],
})
export class AuthModule {}

9️⃣ App Module + Protected Route

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './auth/roles.guard';

@Module({
  imports: [AuthModule],
  providers: [
    { provide: APP_GUARD, useClass: RolesGuard }, // apply global guard
  ],
})
export class AppModule {}

πŸ”Ÿ Contoh Protected Endpoint

Bisa tambahin di app.controller.ts:

import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Roles } from './auth/roles.decorator';

@Controller()
export class AppController {
  @Get('profile')
  @UseGuards(AuthGuard('jwt'))
  getProfile(@Request() req) {
    return req.user; // user dari JWT
  }

  @Get('admin')
  @UseGuards(AuthGuard('jwt'))
  @Roles('admin')
  getAdminData() {
    return { message: 'Hanya admin yang bisa lihat ini πŸš€' };
  }
}

πŸš€ Cara Coba

1. Jalankan server:

npm run start:dev

2. Login:

POST http://localhost:3000/auth/login
body: { "username": "bob", "password": "1234" }

β†’ dapet token.

3. Akses endpoint pakai header:

Authorization: Bearer <token>
  • /profile β†’ bisa diakses semua user login.
  • /admin β†’ cuma bisa diakses user role admin.

🎯 Kesimpulan

  • Authentication (login pakai JWT) = tiket masuk.
  • Authorization (role guard) = gelang VIP.
  • Dengan project mini ini, lo bisa langsung coba konsep authN + authZ di NestJS.

Abdan Zam Zam Ramadhan
Abdan Zam Zam Ramadhan

Senior Software Engineer @ PT. Astra Internasional, Tbk.

Articles: 11