Belajar Backend Developer - Authentication & Authorization
Episode 7 of 28

Belajar Backend Developer - Authentication & Authorization

Mengamankan akses API dari nol: hashing password yang benar, sesi berbasis token JWT, flow OAuth2/OIDC untuk integrasi pihak ketiga, model otorisasi RBAC, dan langkah-langkah implementasi autentikasi end-to-end

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Setelah di episode 4-6 kalian punya API dan data, sekarang waktunya menjawab dua pertanyaan keamanan paling mendasar: siapa kalian? (authentication) dan apa yang boleh kalian lakukan? (authorization). Dua konsep ini sering dicampur, padahal berbeda dan keduanya wajib.

Mengapa ini krusial? Karena hampir semua kebocoran data besar berawal dari autentikasi yang lemah: password yang di-hash dengan salah, token yang mudah dipalsukan, atau otorisasi yang hanya dicek di frontend. Episode ini membangun autentikasi end-to-end yang benar — fondasi yang akan kalian perkuat di episode 17-19 (security) dan 23 (AI backend, yang juga butuh auth untuk akses model).

Authentication vs Authorization

100%
  • Authentication — memverifikasi identitas: login dengan email + password, atau SSO.
  • Authorization — menentukan izin setelah identitas diketahui: user biasa vs admin.

Urutannya selalu authentication dulu, baru authorization. API yang melakukan kebalikannya (cek role sebelum cek login) adalah bug keamanan.

Keamanan Password

Sebelum membahas token, fondasi pertama: password tidak boleh disimpan sebagai teks atau di-hash dengan hash cepat. Gunakan algoritma yang sengaja dibuat lambat (key derivation function) seperti Argon2id atau bcrypt.

Hash dan verifikasi password dengan Argon2
import { hash, verify } from "argon2"
 
export async function hashPassword(plain: string): Promise<string> {
  return hash(plain, {
    type: "argon2id",
    memoryCost: 65536,
    timeCost: 3,
    parallelism: 4,
  })
}
 
export async function checkPassword(
  plain: string,
  hashed: string,
): Promise<boolean> {
  return verify(hashed, plain)
}

Aturan praktis:

  • Selalu hash dengan Argon2id/bcrypt — jangan pernah MD5/SHA-1 untuk password.
  • Jangan pernah log password, termasuk saat debugging.
  • Beri rate limit pada endpoint login (episode 20) agar brute-force melambat.

Session vs JWT

Dua pendekatan utama menyimpan state autentikasi:

Session (Stateful)

Server menyimpan sesi di database/Redis, client hanya memegang session_id. Kelebihan: server bisa mencabut sesi kapan saja. Kekurangan: state harus disimpan dan di-sinkronkan antar instance.

Sesi berbasis Redis
import { Redis } from "ioredis"
import { randomBytes } from "node:crypto"
 
const redis = new Redis(process.env.REDIS_URL!)
 
export async function createSession(userId: number) {
  const sessionId = randomBytes(32).toString("hex")
  await redis.set(`session:${sessionId}`, String(userId), "EX", 60 * 60 * 24)
  return sessionId
}
 
export async function getSessionUserId(sessionId: string) {
  return redis.get(`session:${sessionId}`)
}

JWT (Stateless)

JWT adalah token self-contained: payload (misal userId dan role) ditandatangani dengan secret, tanpa disimpan di server. Kelebihan: scalable tanpa state bersama. Kekurangan: token tidak bisa dicabut sebelum kadaluarsa — solusinya TTL pendek + refresh token.

Membuat dan verifikasi JWT
import { SignJWT, jwtVerify } from "jose"
 
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
 
export async function signAccessToken(payload: {
  userId: number
  role: string
}) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("15m")
    .sign(secret)
}
 
export async function verifyAccessToken(token: string) {
  const { payload } = await jwtVerify(token, secret)
  return payload
}

Note

Aturan praktis 2026: JWT untuk API yang stateless dan perlu scale-out, session untuk aplikasi yang butuh revoke instan. Banyak sistem modern memakai keduanya: JWT berumur pendek (15 menit) untuk request, refresh token berumur panjang di storage aman. Jangan pernah memasukkan data sensitif ke payload JWT — payload bisa dibaca siapa saja, hanya tandatangannya yang terproteksi.

Implementasi End-to-End

1. Endpoint Register dan Login

routes/auth.ts
import { Router } from "express"
import { hashPassword, checkPassword } from "../security/password.js"
import { signAccessToken } from "../security/jwt.js"
 
const router = Router()
 
router.post("/register", async (req, res, next) => {
  try {
    const { email, password } = req.body
    const hashed = await hashPassword(password)
    const user = await createUser(email, hashed)
    res.status(201).json({ data: { id: user.id, email: user.email } })
  } catch (err) {
    next(err)
  }
})
 
router.post("/login", async (req, res, next) => {
  try {
    const { email, password } = req.body
    const user = await findUserByEmail(email)
    if (!user || !(await checkPassword(password, user.passwordHash))) {
      return res.status(401).json({ error: "INVALID_CREDENTIALS" })
    }
    const token = await signAccessToken({ userId: user.id, role: user.role })
    res.json({ data: { token } })
  } catch (err) {
    next(err)
  }
})
 
export default router

2. Middleware Autentikasi

middleware/auth.ts
import type { NextFunction, Request, Response } from "express"
import { verifyAccessToken } from "../security/jwt.js"
 
export async function requireAuth(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const header = req.headers.authorization
  const token = header?.startsWith("Bearer ") ? header.slice(7) : null
  if (!token) {
    return res.status(401).json({ error: "UNAUTHORIZED", message: "Token diperlukan" })
  }
  try {
    req.user = await verifyAccessToken(token)
    next()
  } catch {
    res.status(401).json({ error: "INVALID_TOKEN", message: "Token tidak valid" })
  }
}

Token dikirim via header Authorization: Bearer <token> dan diverifikasi di setiap request yang terproteksi.

3. RBAC: Otorisasi Berbasis Role

Setelah autentikasi, authorization menentukan akses. RBAC (Role-Based Access Control) memetakan user ke role, role ke izin.

middleware/rbac.ts
const ROLE_PERMISSIONS: Record<string, string[]> = {
  customer: ["order:create", "order:read:own"],
  admin: ["*"],
}
 
export function requirePermission(permission: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const role = req.user?.role ?? "anonymous"
    if (!ROLE_PERMISSIONS[role].includes("*") &&
        !ROLE_PERMISSIONS[role].includes(permission)) {
      return res.status(403).json({ error: "FORBIDDEN", message: "Tidak diizinkan" })
    }
    next()
  }
}

Pemakaiannya di route:

Route dengan RBAC
router.post("/orders", requireAuth, requirePermission("order:create"), createOrder)

Perhatikan lapisan ini: requireAuth menjawab siapa, requirePermission menjawab boleh apa. Keduanya berjalan berurutan.

OAuth2 dan OIDC

Saat aplikasi butuh "Login with Google" atau memberi akses ke pihak ketiga, kalian tidak menulis login sendiri — pakai OAuth2/OIDC.

  • OAuth2 — protokol pemberian izin: resource owner memberikan akses terbatas ke aplikasi lain lewat access token.
  • OIDC (OpenID Connect) — lapisan identitas di atas OAuth2 yang menambahkan ID token berisi informasi user.

Alur Authorization Code (paling aman untuk web app):

100%

Kunci keamanan: code ditukar di server-side, bukan di browser — access token tidak pernah terekspos ke JavaScript.

Common Pitfalls

Password di-Hash dengan Hash Cepat

MD5/SHA-1/SHA-256 untuk password bisa dipecahkan dengan GPU dalam hitungan detik. Wajib Argon2id/bcrypt.

JWT Tanpa Kadaluarsa

Token tanpa exp selamanya valid. Selalu set TTL pendek dan sediakan refresh token.

Check Auth Hanya di Frontend

Menyembunyikan tombol "Admin" di UI bukan otorisasi. Semua pemeriksaan izin harus di backend, di tiap route.

Login Tanpa Rate Limit

Endpoint login yang bisa dicoba jutaan kali adalah pintu brute-force. Episode 20 menutup celah ini.

Warning

Jangan pernah menyimpan JWT_SECRET di kode atau commit ke git — simpan di environment variable/secret manager. Secret yang bocor berarti siapa pun bisa memalsukan token dan mengakses akun apa pun. Episode 17 membahas pengelolaan secrets secara menyeluruh.

Penutup

Episode 7 membangun autentikasi end-to-end: hashing password dengan Argon2id, dua pendekatan state (session vs JWT), implementasi register/login, middleware requireAuth, RBAC untuk otorisasi, dan alur OAuth2/OIDC.

Inti yang harus dibawa pulang:

  • Authentication = siapa; Authorization = boleh apa. Kerjakan berurutan.
  • Password wajib di-hash Argon2id/bcrypt; jangan pernah log password.
  • JWT stateless dengan TTL pendek untuk API; session untuk revoke instan.
  • RBAC memetakan role ke izin; periksa di backend, bukan frontend.
  • OAuth2/OIDC untuk integrasi pihak ketiga; tukar code di server-side.

Di episode 8 selanjutnya kita akan memastikan semua ini benar-benar bekerja: testing backend — unit test, integration test, contract test, dan end-to-end test, lengkap dengan membangun test suite API. Sampai jumpa di episode 8!

Belajar Backend Developer - Authentication & Authorization | Belajar Backend