Autentikasi JWT di Next.js: Implementasi Login dan Token Refresh yang Aman

Lhuqita Fazry
Web Development Next.js JWT Authentication
Autentikasi JWT di Next.js: Implementasi Login dan Token Refresh yang Aman

Autentikasi berbasis JWT (JSON Web Token) merupakan pendekatan populer untuk mengamankan aplikasi web modern. Next.js menyediakan infrastruktur yang ideal untuk mengimplementasikan sistem ini — API Routes menangani backend logic, sementara Middleware mengelola proteksi route secara efisien. Kombinasi access token dan refresh token memungkinkan user experience yang seamless tanpa mengorbankan keamanan.

Karakteristik stateless dari JWT membuatnya sangat cocok untuk arsitektur serverless yang digunakan Next.js. Setiap request membawa token yang berisi informasi user dan expiry time, sehingga server tidak perlu menyimpan session state. Tantangan utama terletak pada manajemen token yang tepat: access token harus berumur pendek untuk membatasi risiko jika bocor, sementara refresh token memerlukan perlindungan ekstra karena memiliki lifespan yang lebih panjang.

Setup Project dan Dependencies

Mulai dengan membuat project Next.js dan menginstall library yang diperlukan untuk JWT handling. Kita menggunakan jsonwebtoken untuk signing dan verification, serta cookie untuk mengelola HTTP-only cookies secara terstruktur.

bashbash
npx create-next-app@latest jwt-auth-app --typescript --tailwind

# Install dependencies untuk JWT dan cookie handling
npm install jsonwebtoken cookie
npm install -D @types/jsonwebtoken @types/cookie

File .env.local perlu dikonfigurasi dengan secret keys yang kuat. Access token secret dan refresh token secret harus berbeda untuk membatasi impact jika salah satu secret ter-compromise.

bashbash
# .env.local
JWT_ACCESS_SECRET=your-strong-access-secret-min-32-chars
JWT_REFRESH_SECRET=your-strong-refresh-secret-min-32-chars
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d

Implementasi Login API

API Route untuk login memvalidasi kredensial user, menghasilkan pasangan access token dan refresh token, serta menyimpan refresh token di HTTP-only cookie. Pendekatan ini mencegah XSS attack karena cookie tidak dapat diakses oleh JavaScript di browser.

typescripttypescript
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { email, password } = body;

  // Validasi kredensial (replace dengan database query)
  const user = await validateCredentials(email, password);
  if (!user) {
    return NextResponse.json(
      { error: 'Kredensial tidak valid' },
      { status: 401 }
    );
  }

  const payload = { userId: user.id, email: user.email };

  const accessToken = jwt.sign(payload, process.env.JWT_ACCESS_SECRET!, {
    expiresIn: process.env.ACCESS_TOKEN_EXPIRY,
  });

  const refreshToken = jwt.sign(
    { userId: user.id },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: process.env.REFRESH_TOKEN_EXPIRY }
  );

  // Simpan refresh token ke database untuk invalidation nanti
  await storeRefreshToken(user.id, refreshToken);

  const response = NextResponse.json({ accessToken });

  response.headers.set(
    'Set-Cookie',
    cookie.serialize('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 60 * 60 * 24 * 7, // 7 hari
      path: '/',
    })
  );

  return response;
}
Fullstack Web Development With Next.js
Web App • Beginner

Fullstack Web Development With Next.js

A practical, beginner-friendly, and project-based introduction to full-stack web...

Daftar

Fungsi validateCredentials memeriksa email dan password terhadap database. Jika validasi berhasil, jwt.sign membuat access token dengan expiry 15 menit dan refresh token dengan expiry 7 hari. Refresh token disimpan ke database agar dapat di-revoke jika diperlukan. Cookie dikonfigurasi dengan flag httpOnly, secure, dan sameSite=strict untuk mitigasi XSS dan CSRF.

Implementasi Token Refresh

Endpoint refresh token memeriksa validity refresh token dari cookie, memverifikasi terhadap database, lalu menghasilkan access token baru. Mekanisme ini memungkinkan user tetap terautentikasi tanpa harus login ulang setiap 15 menit.

typescripttypescript
// app/api/auth/refresh/route.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import cookie from 'cookie';

export async function POST(request: NextRequest) {
  const cookies = cookie.parse(request.headers.get('cookie') || '');
  const refreshToken = cookies.refreshToken;

  if (!refreshToken) {
    return NextResponse.json(
      { error: 'Refresh token tidak ditemukan' },
      { status: 401 }
    );
  }

  try {
    const decoded = jwt.verify(
      refreshToken,
      process.env.JWT_REFRESH_SECRET!
    ) as { userId: string };

    // Verifikasi refresh token ada di database
    const storedToken = await getRefreshToken(decoded.userId);
    if (storedToken !== refreshToken) {
      return NextResponse.json(
        { error: 'Refresh token tidak valid' },
        { status: 403 }
      );
    }

    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.JWT_ACCESS_SECRET!,
      { expiresIn: process.env.ACCESS_TOKEN_EXPIRY }
    );

    return NextResponse.json({ accessToken: newAccessToken });
  } catch (error) {
    return NextResponse.json(
      { error: 'Refresh token expired atau invalid' },
      { status: 403 }
    );
  }
}

Verifikasi refresh token dilakukan dalam dua tahap: pertama dengan jwt.verify untuk memastikan signature dan expiry valid, kedua dengan membandingkan token dari cookie dengan token yang tersimpan di database. Jika refresh token tidak cocok, sistem mengembalikan error 403 yang memaksa user untuk login ulang. Rotasi refresh token dapat ditambahkan dengan menghasilkan refresh token baru setiap kali refresh dilakukan.

Middleware dan Route Protection

Next.js Middleware berjalan di Edge Runtime sebelum request mencapai page atau API Route. Middleware ini memverifikasi access token dan melakukan redirect ke halaman login jika token tidak valid atau sudah expired.

typescripttypescript
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';

export function middleware(request: NextRequest) {
  const protectedPaths = ['/dashboard', '/profile', '/settings'];
  const { pathname } = request.nextUrl;

  if (!protectedPaths.some((path) => pathname.startsWith(path))) {
    return NextResponse.next();
  }

  const authHeader = request.headers.get('authorization');
  const token = authHeader?.replace('Bearer ', '');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    jwt.verify(token, process.env.JWT_ACCESS_SECRET!);
    return NextResponse.next();
  } catch (error) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*'],
};

Middleware ini memeriksa apakah request mengarah ke protected path. Jika ya, access token diekstrak dari header Authorization dan diverifikasi dengan secret key. Validasi gagal akan mengarahkan user ke halaman login. Konfigurasi matcher memastikan middleware hanya berjalan pada route yang memerlukan proteksi, sehingga tidak menambah overhead pada route publik.

Client-Side Token Management

Di sisi client, access token disimpan di memory (React state atau Context) untuk menghindari XSS risk dari localStorage. Client menggunakan interceptor untuk menangani 401 response dan memicu refresh token secara otomatis.

typescripttypescript
// lib/api-client.ts
let accessToken: string | null = null;

export function setAccessToken(token: string) {
  accessToken = token;
}

export async function apiClient(url: string, options: RequestInit = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      Authorization: accessToken ? `Bearer ${accessToken}` : '',
      'Content-Type': 'application/json',
    },
  });

  if (response.status === 401) {
    const refreshResponse = await fetch('/api/auth/refresh', {
      method: 'POST',
      credentials: 'include',
    });

    if (!refreshResponse.ok) {
      window.location.href = '/login';
      return;
    }

    const { accessToken: newToken } = await refreshResponse.json();
    setAccessToken(newToken);

    // Retry request original dengan token baru
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${newToken}`,
        'Content-Type': 'application/json',
      },
    });
  }

  return response;
}

Access token disimpan sebagai variabel global di module scope. Ketika API mengembalikan status 401, client secara otomatis memanggil endpoint /api/auth/refresh dengan cookie yang berisi refresh token. Jika refresh berhasil, request original diulang dengan access token yang baru. Jika refresh gagal, user diarahkan ke halaman login. Pattern ini menghilangkan kebutuhan user untuk menangani expired token secara manual.

Best Practices Keamanan

Beberapa praktik keamanan harus diimplementasikan untuk meminimalkan risk pada sistem autentikasi JWT. Refresh token harus disimpan di database dengan kemampuan revocation — token yang dicurigai compromised dapat dihapus sehingga user terpaksa login ulang. Access token sebaiknya memiliki expiry yang singkat, idealnya 5 hingga 15 menit, untuk membatasi window of opportunity jika token di-intercept.

HTTPS wajib digunakan di environment production untuk mencegah token sniffing. Cookie harus selalu mengaktifkan flag secure dan sameSite=strict untuk mitigasi CSRF. Selain itu, payload JWT tidak boleh menyimpan data sensitive seperti password atau informasi pribadi yang bersifat rahasia karena payload JWT hanya di-encode, bukan di-enkripsi.

Mau memperdalam skill Web Development secara sistematis? Bergabunglah dengan Web Development Bootcamp di Rumah Coding. Kurikulum praktis dengan proyek real-world dan mentorship dari praktisi industri.

Course Terkait

TechConnect - Modern IT Job Portal
Premium Course Web App

Fullstack Web Development With Next.js

A practical, beginner-friendly, and project-based introduction to full-stack web development. Students will learn to build, secure, and deploy modern web applications from scratch using Next.js (App Router), React, Tailwind CSS, and a relational database. By the end of the course, students will have a fully functional, production-ready application to showcase in their portfolio.

Capstone Project

TechConnect - Modern IT Job Portal

  • Public Job Board: A responsive homepage displaying available job listings with dynamic routing for individual job detail pages.
  • Search & Filter: Basic functionality allowing users to find jobs based on keywords or categories.
  • User Authentication: Secure Sign Up, Log In, and Log Out workflows.
7 Weeks Beginner
Lihat Detail Course

Artikel Terkait