Use Cases (Diperbarui: 2/6/2026)

Autentikasi Aman dengan Claude Code: Session Next.js, Batas JWT, dan OAuth

Bangun auth aman dengan Claude Code: session Next.js, JWT, OAuth, CSRF, RBAC, audit log, dan test.

Autentikasi Aman dengan Claude Code: Session Next.js, Batas JWT, dan OAuth

Autentikasi bukan sekadar form login. Sebelum rilis, kamu perlu memikirkan penyimpanan password, session cookie, JWT, OAuth callback, CSRF, reset password, RBAC, secrets, audit log, dan test. Jika Claude Code hanya diminta “tambahkan auth”, hasilnya bisa terlihat berjalan, tetapi desain production masih menyimpan risiko.

Shortcut berbahaya yang paling sering muncul adalah menyimpan JWT panjang di browser localStorage. Jika ada XSS, yaitu JavaScript berbahaya yang berjalan di halaman, token itu bisa dibaca dan dipakai ulang. Untuk web app biasa, default yang lebih mudah diaudit adalah server-side session: server menyimpan status login, browser hanya menerima session ID acak lewat cookie HttpOnly.

Artikel ini memakai Next.js App Router dan menyediakan demo yang bisa disalin: validasi Zod, password hashing bcrypt, signed opaque session cookie, middleware guard, CSRF dan Origin check, RBAC, audit log, serta Vitest test. JWT dibahas sebagai batas API atau mobile yang singkat. OAuth dipakai sebagai batas identity provider, bukan pengganti authorization lokal.

Gunakan referensi resmi sebagai sumber utama: Next.js Authentication guide, Next.js cookies API, OWASP Authentication Cheat Sheet, OWASP Password Storage Cheat Sheet, OWASP Forgot Password Cheat Sheet, MDN Secure cookie configuration, Auth.js, dan Claude Code docs. Untuk konteks tambahan, baca cookie management, RBAC, dan Zod validation.

Pisahkan Session, JWT, dan OAuth

Session menjawab “apakah browser ini masih login?” JWT menjawab “apakah client ini bisa membuktikan klaim bertanda tangan dalam waktu singkat?” OAuth/OIDC menjawab “apakah provider tepercaya sudah mengautentikasi user ini?” Jika tiga hal ini dicampur, review menjadi sulit.

MekanismeCocok untukKekuatanRisiko
Server-side sessionDashboard SaaS, member area, adminRevocation dan audit lebih mudahPerlu Redis, Postgres, DynamoDB, atau store lain
JWTMobile API, service call singkat, API eksternalVerifikasi signature tanpa DB lookupRevocation sulit; hindari penyimpanan panjang di browser
OAuth / OIDCGoogle, GitHub, corporate SSOPassword user tidak disimpan sendiriLogin provider tidak mengganti permission lokal

Tiga use case sering muncul. Dashboard SaaS memakai HttpOnly session cookie dan meminta reauthentication sebelum perubahan billing atau email. Situs konten berbayar memisahkan free reader, buyer, editor, dan admin dengan RBAC serta audit log. Tool internal B2B boleh memakai Google Workspace atau Entra ID untuk login, tetapi tenant boundary, role, expiry, dan revocation tetap dicek aplikasi.

Prompt Aman untuk Claude Code

Berikan kontrak keamanan sebelum meminta kode.

Implementasikan authentication untuk Next.js App Router.
Syarat:
- Browser login memakai server-side session dan HttpOnly Cookie
- JWT hanya untuk token API eksternal yang short-lived; jangan pakai localStorage
- Password di-hash dengan bcrypt atau Argon2id, tidak plaintext
- Input divalidasi dengan Zod dan tidak membocorkan apakah email ada
- Cookie menetapkan Secure, HttpOnly, SameSite, Path, dan Max-Age
- API yang mengubah state memeriksa Origin dan CSRF token
- OAuth memakai Auth.js atau library matang, bukan implementasi manual penuh
- Sertakan RBAC, password reset, audit logs, dan tests
- Akhiri dengan pitfalls dan verification commands

Prompt ini memaksa Claude Code bekerja di dalam batas yang bisa direview. Untuk autentikasi, melarang shortcut berbahaya sama pentingnya dengan menulis route handler.

Implementasi Minimal Next.js

Instal dependency.

npm install zod bcryptjs
npm install -D vitest typescript @types/node

Tambahkan secret minimal 32 karakter di .env.local. Jangan commit secret ini.

SESSION_SECRET="replace-with-at-least-32-random-characters"

File lib/auth/password.ts. OWASP merekomendasikan Argon2id sebagai pilihan modern; demo ini memakai bcryptjs agar mudah disalin.

import bcrypt from "bcryptjs";
import { z } from "zod";

export const passwordSchema = z.string().min(12).max(128);

export async function hashPassword(password: string) {
  const parsed = passwordSchema.parse(password);
  return bcrypt.hash(parsed, 12);
}

export async function verifyPassword(password: string, hash: string) {
  return bcrypt.compare(password, hash);
}

File lib/auth/session.ts. Map memori hanya untuk demo; production perlu Redis, PostgreSQL, DynamoDB, atau store bersama.

import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { z } from "zod";

const env = z
  .object({
    NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
    SESSION_SECRET: z.string().min(32),
  })
  .parse(process.env);

export type Role = "user" | "admin";
type SessionRecord = { userId: string; role: Role; csrfToken: string; expiresAt: number };

declare global {
  var demoSessions: Map<string, SessionRecord> | undefined;
}

const sessions = globalThis.demoSessions ?? new Map<string, SessionRecord>();
globalThis.demoSessions = sessions;

export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8;
export const SESSION_COOKIE_NAME =
  env.NODE_ENV === "production" ? "__Host-session" : "dev-session";
export const sessionCookieOptions = {
  httpOnly: true,
  secure: env.NODE_ENV === "production",
  sameSite: "lax" as const,
  path: "/",
  maxAge: SESSION_MAX_AGE_SECONDS,
};

function signSessionId(sessionId: string) {
  return createHmac("sha256", env.SESSION_SECRET).update(sessionId).digest("base64url");
}

function safeEqual(left: string, right: string) {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  return a.length === b.length && timingSafeEqual(a, b);
}

export function createSession(userId: string, role: Role = "user") {
  const sessionId = randomBytes(32).toString("base64url");
  const token = `${sessionId}.${signSessionId(sessionId)}`;
  const csrfToken = randomBytes(32).toString("base64url");
  sessions.set(sessionId, {
    userId,
    role,
    csrfToken,
    expiresAt: Date.now() + SESSION_MAX_AGE_SECONDS * 1000,
  });
  return { token, csrfToken };
}

export function getSession(token?: string) {
  if (!token) return null;
  const [sessionId, signature] = token.split(".");
  if (!sessionId || !signature || !safeEqual(signature, signSessionId(sessionId))) return null;
  const session = sessions.get(sessionId);
  if (!session || session.expiresAt < Date.now()) {
    sessions.delete(sessionId);
    return null;
  }
  return { id: sessionId, ...session };
}

export function destroySession(token?: string) {
  const sessionId = token?.split(".")[0];
  if (sessionId) sessions.delete(sessionId);
}

export function assertSameOrigin(request: Request) {
  const origin = request.headers.get("origin");
  if (origin && origin !== new URL(request.url).origin) throw new Error("Bad origin");
}

export function assertCsrf(request: Request, session: { csrfToken: string }) {
  const submitted = request.headers.get("x-csrf-token");
  if (!submitted || submitted !== session.csrfToken) throw new Error("Bad CSRF token");
}

Route login app/api/login/route.ts. Di aplikasi nyata, passwordHash dan role berasal dari database.

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { hashPassword, verifyPassword } from "@/lib/auth/password";
import { SESSION_COOKIE_NAME, createSession, sessionCookieOptions } from "@/lib/auth/session";

export const runtime = "nodejs";
export const loginInputSchema = z.object({
  email: z.string().trim().toLowerCase().email(),
  password: z.string().min(12).max(128),
});

async function findUserByEmail(email: string) {
  if (email !== "masa@example.com") return null;
  return {
    id: "user_123",
    role: "admin" as const,
    passwordHash: await hashPassword("correct-horse-battery-staple"),
  };
}

export async function POST(request: NextRequest) {
  const parsed = loginInputSchema.safeParse(await request.json());
  if (!parsed.success) return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });

  const user = await findUserByEmail(parsed.data.email);
  const passwordOk = user ? await verifyPassword(parsed.data.password, user.passwordHash) : false;
  if (!user || !passwordOk) {
    return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
  }

  const session = createSession(user.id, user.role);
  const response = NextResponse.json({ ok: true, csrfToken: session.csrfToken });
  response.cookies.set({ name: SESSION_COOKIE_NAME, value: session.token, ...sessionCookieOptions });
  response.cookies.set({
    name: "csrf-token",
    value: session.csrfToken,
    secure: sessionCookieOptions.secure,
    sameSite: "lax",
    path: "/",
    maxAge: sessionCookieOptions.maxAge,
  });
  return response;
}

middleware.ts hanya guard untuk navigasi. Authorization final tetap di server route.

import { NextRequest, NextResponse } from "next/server";

const SESSION_COOKIE_NAME =
  process.env.NODE_ENV === "production" ? "__Host-session" : "dev-session";

export function middleware(request: NextRequest) {
  const hasSession = request.cookies.has(SESSION_COOKIE_NAME);
  const pathname = request.nextUrl.pathname;
  if (!hasSession && (pathname.startsWith("/dashboard") || pathname.startsWith("/admin"))) {
    return NextResponse.redirect(new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = { matcher: ["/dashboard/:path*", "/admin/:path*"] };

Test test/auth.test.ts.

import { beforeAll, describe, expect, it } from "vitest";

beforeAll(() => {
  process.env.NODE_ENV = "test";
  process.env.SESSION_SECRET = "test-secret-value-with-more-than-32-characters";
});

describe("auth primitives", () => {
  it("hashes and verifies passwords", async () => {
    const { hashPassword, verifyPassword } = await import("../lib/auth/password");
    const hash = await hashPassword("correct-horse-battery-staple");
    await expect(verifyPassword("correct-horse-battery-staple", hash)).resolves.toBe(true);
    await expect(verifyPassword("wrong-password", hash)).resolves.toBe(false);
  });

  it("creates and destroys a session", async () => {
    const { createSession, destroySession, getSession } = await import("../lib/auth/session");
    const session = createSession("user_123", "admin");
    expect(getSession(session.token)?.role).toBe("admin");
    destroySession(session.token);
    expect(getSession(session.token)).toBeNull();
  });

  it("validates login input", async () => {
    const { loginInputSchema } = await import("../app/api/login/route");
    expect(loginInputSchema.safeParse({ email: "bad", password: "short" }).success).toBe(false);
  });
});

Password Reset, OAuth, dan JWT

Reset password tidak boleh membocorkan apakah email terdaftar. Gunakan pesan yang sama, buat token acak kuat, simpan hanya hash token, beri expiry singkat, dan jadikan single-use. Setelah password berubah, sediakan opsi untuk invalidasi session lama.

OAuth sebaiknya tidak ditulis manual. Gunakan Auth.js atau library matang, hubungkan provider identity ke user lokal, lalu buat session cookie aplikasi sendiri. Provider access token bukan cookie session aplikasi.

JWT berguna untuk API, tetapi bukan jawaban untuk semua auth. Jika dipakai, tetapkan aud, iss, exp, key rotation, dan respons saat token bocor.

Pitfalls, Audit Log, dan Monetisasi

Sebelum deploy, blokir kesalahan ini: JWT panjang di localStorage, cookie tanpa Secure atau HttpOnly, password hash SHA-256 biasa, reset token plaintext, error login yang membuka apakah email ada, RBAC hanya di middleware, dan log yang mencetak secret.

Audit log berisi actor, action, result, dan waktu. Jangan simpan password, session ID, reset token, atau OAuth access token. Untuk SaaS, cek tenantId sebelum role.

Autentikasi juga menjaga revenue path: konten premium, template, link Gumroad, form B2B, dan dashboard member. Mulai dari checklist gratis untuk membakukan review Claude Code. Jika perlu resource reusable, lihat products/templates. Untuk desain auth, RBAC, audit log, dan CI test di repo nyata, gunakan Claude Code training/consultation.

Hasil Praktik

Saat Masa mencoba alur ini, nilai utamanya bukan login route saja. Session cookie, CSRF, RBAC, audit log, dan test menjadi satu unit kerja. Versi awal membuat middleware terlihat seperti security boundary, padahal hanya mengecek keberadaan cookie. Memindahkan check nyata ke server route membuat review lebih jelas dan mengurangi risiko admin page terbuka.

Ringkasan

Saat membangun auth dengan Claude Code, mulai dari batasnya. Browser memakai server-side session dan secure cookie. API memakai JWT singkat. Login eksternal memakai OAuth/OIDC library. State change memakai CSRF dan Origin. Authorization memakai RBAC. Aksi sensitif memakai audit log dan test.

#Claude Code #authentication #Next.js #JWT #OAuth #security
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.