Terakhir diperbarui: 2024-05-17
Penulis: Tim PBF 2024
Pada codelab ini Anda akan mempelajari tentang authentication di ReactJS dengan Next.js.
Sebelum memulai codelab ini, sebaiknya Anda memiliki pengetahuan dasar tentang:
useFormStatus
dan useFormState
untuk menangani pending states dan error pada formOtentikasi (authentication) adalah bagian penting dari banyak aplikasi web saat ini. Begitulah cara sistem memeriksa apakah pengguna memang sesuai dengan yang sebenarnya atau bukan.
Situs web yang aman sering kali menggunakan berbagai cara untuk memeriksa identitas pengguna. Misalnya, setelah memasukkan nama pengguna dan kata sandi, situs mungkin mengirimkan kode verifikasi ke perangkat Anda atau menggunakan aplikasi eksternal seperti Google Authenticator. Otentikasi 2 faktor (2FA) ini membantu meningkatkan keamanan. Meskipun seseorang mengetahui kata sandi Anda, mereka tidak dapat mengakses akun Anda tanpa token unik Anda.
Dalam pengembangan web, otentikasi dan otorisasi mempunyai peran yang berbeda:
Jadi, autentikasi memeriksa siapa Anda, dan otorisasi menentukan apa yang dapat Anda lakukan atau akses dalam aplikasi.
Pada praktikum ini, Anda dapat melanjutkan project dari Codelab #09 sebelumnya. Silakan lakukan langkah-langkah praktikum berikut ini.
Buat route baru dengan nama /login
dan paste kode berikut:
/app/login/page.tsx
import AcmeLogo from '@/app/ui/acme-logo';
import LoginForm from '@/app/ui/login-form';
export default function LoginPage() {
return (
<main className="flex items-center justify-center md:h-screen">
<div className="relative mx-auto flex w-full max-w-[400px] flex-col space-y-2.5 p-4 md:-mt-32">
<div className="flex h-20 w-full items-end rounded-lg bg-blue-500 p-3 md:h-36">
<div className="w-32 text-white md:w-36">
<AcmeLogo />
</div>
</div>
<LoginForm />
</div>
</main>
);
}
Anda akan melihat error pada impor LoginForm
dan AcmeLogo
, yang akan Anda perbarui nanti di akhir praktikum ini.
Kita akan menggunakan NextAuth.js untuk menambahkan otentikasi ke aplikasi Anda. NextAuth.js mengabstraksi sebagian besar kerumitan yang terlibat dalam pengelolaan sesi, proses masuk dan keluar, serta aspek autentikasi lainnya. Meskipun Anda dapat menerapkan fitur-fitur ini secara manual, prosesnya dapat memakan waktu dan rawan kesalahan. NextAuth.js
menyederhanakan proses, memberikan solusi terpadu untuk autentikasi di aplikasi Next.js.
Install NextAuth
versi terakhir dengan cara running perintah berikut di terminal:
npm i --save next-auth@beta
Selanjutnya, kita generate secret key. Ini digunakan untuk enkripsi cookies, memastikan keamanan terhadap session pengguna. Anda dapat menjalankan perintah berikut.
openssl rand -base64 32
Jika di komputer Anda belum terpasang openssl
, Anda dapat menggunakan versi online di https://generate-random.org/encryption-key-generator
Kemudian tambahkan baris kode berikut pada file .env
dan paste secret key tersebut.
AUTH_SECRET="your-secret-key"
auth.config
Buat file auth.config.ts
pada root project Anda. File ini berisi konfigurasi untuk NextAuth.js
.
/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
};
Anda dapat menggunakan opsi pages
untuk menentukan route custom seperti login, logout, dan halaman error. Hal ini tidak wajib, tetapi dengan menambahkan route pada signIn
: '/login
' ke dalam opsi pages
, pengguna akan diarahkan ke halaman login yang dibuat custom, bukan halaman default NextAuth.js.
Selanjutnya, tambahkan logic untuk melindungi route Anda. Ini akan mencegah user mengakses halaman beranda kita kecuali user telah melakukan login.
/auth.config.ts
import type { NextAuthConfig } from 'next-auth';
export const authConfig = {
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith('/');
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect unauthenticated users to login page
} else if (isLoggedIn) {
return Response.redirect(new URL('/', nextUrl));
}
return true;
},
},
providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;
Callback authorized
digunakan untuk memverifikasi apakah permintaan diotorisasi untuk mengakses halaman melalui Next.js Middleware. Itu dipanggil sebelum permintaan diselesaikan, dan menerima objek dengan properti auth
dan request
. Properti auth
berisi sesi pengguna, dan properti request
berisi permintaan masuk.
Bagian providers
adalah sebuah larik tempat Anda mencantumkan opsi login yang berbeda. Untuk saat ini, bagian larik ini biarkan kosong untuk memenuhi konfigurasi NextAuth. Anda akan mempelajarinya lebih lanjut setelah langkah ini.
Selanjutnya, Anda perlu mengimpor objek authConfig
ke dalam file Middleware
. Di root proyek Anda, buat file bernama middleware.ts
dan ketik kode seperti berikut:
/middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};
Di sini Anda menginisialisasi NextAuth.js dengan objek authConfig
dan mengekspor properti auth
. Anda juga menggunakan opsi matcher
dari Middleware untuk menentukan bahwa opsi tersebut harus dijalankan pada jalur tertentu.
Keuntungan menggunakan Middleware adalah rute yang dilindungi tidak akan mulai dirender sampai Middleware memverifikasi otentikasi, sehingga dapat meningkatkan keamanan dan kinerja aplikasi Anda.
Merupakan praktik yang baik untuk meng-hash kata sandi sebelum menyimpannya ke dalam database. Hashing mengubah kata sandi menjadi serangkaian karakter dengan panjang tetap, yang tampak acak, memberikan lapisan keamanan bahkan jika data pengguna terekspos.
Di file src/seeder/seed.js
pada project codelab sebelumnya, Anda menggunakan paket bernama bcrypt
untuk meng-hash kata sandi pengguna sebelum menyimpannya ke database. Anda akan menggunakannya lagi nanti pada praktikum ini untuk membandingkan bahwa kata sandi yang dimasukkan oleh pengguna cocok dengan yang ada di database. Namun, Anda perlu membuat file terpisah untuk paket bcrypt
. Ini karena bcrypt
bergantung pada API Node.js
yang tidak tersedia di Middleware Next.js
.
Buat file baru bernama auth.ts
pada root project dengan berisi kode berikut:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const { auth, signIn, signOut } = NextAuth({
...authConfig,
});
Selanjutnya, Anda perlu menambahkan providers
untuk NextAuth.js
. Providers
adalah larik tempat Anda mencantumkan opsi masuk yang berbeda seperti Google atau GitHub. Untuk praktikum ini, kami akan fokus menggunakan Credentials provider
saja.
Credentials provider
memungkinkan pengguna untuk masuk dengan nama pengguna dan kata sandi. Tambahkan kode pada file auth.ts
di baris 3 dan 7 seperti berikut.
Install dependensi yang dibutuhkan dengan perintah berikut di terminal.
npm i --save zod
Anda dapat menggunakan fungsi authorize
untuk menangani logika otentikasi. Mirip dengan Server Actions
, Anda dapat menggunakan library zod
untuk memvalidasi penulisan email dan kata sandi sebelum memeriksa apakah pengguna ada di database. Tambahkan kode pada baris 4 dan baris 9-15 seperti berikut:
Setelah memvalidasi kredensial, buat fungsi getUser
baru yang dapat melakukan query pengguna dari database.
Lalu, panggil bcrypt.compare
untuk memeriksa apakah kata sandinya cocok:
Terakhir, jika kata sandi cocok, kembalikan pengguna, jika tidak, kembalikan nilai null
untuk mencegah pengguna masuk.
Melanjutkan dari praktikum sebelumnya, sekarang Anda perlu persiapkan untuk tampilan form login dan fungsi sign in. Silakan lakukan langkah-langkah berikut ini.
actions authenticate
Sekarang Anda perlu menghubungkan logika autentikasi dengan formulir login Anda. Di file src/model/actions.tsx
Anda, buat action
baru yang disebut authenticate
. Actions ini harus mengimpor fungsi signIn
dari file auth.ts
:
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
// ...
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}
Jika terjadi error 'CredentialsSignin'
, Anda dapat menampilkan pesan error yang sesuai. Anda dapat mempelajari error NextAuth.js
dengan akses dokumentasi.
login-form.tsx
Berikutnya, buatlah file baru pada path src/app/components/molecules/login-form.tsx
, Anda dapat menggunakan useFormState
React untuk memanggil action server dan menangani error pada login form, dan menggunakan useFormStatus
untuk menangani state
pada login form:
'use client';
import { lusitana } from '@/app/components/atoms/fonts';
import {
AtSymbolIcon,
KeyIcon,
ExclamationCircleIcon,
} from '@heroicons/react/24/outline';
import { ArrowRightIcon } from '@heroicons/react/20/solid';
import { Button } from '@/app/components/atoms/button';
import { useFormState, useFormStatus } from 'react-dom';
import { authenticate } from '@/model/actions';
export default function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);
return (
<form action={dispatch} className="space-y-3">
<div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
<h1 className={`${lusitana.className} mb-3 text-2xl`}>
Please log in to continue.
</h1>
<div className="w-full">
<div>
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="email"
>
Email
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="email"
type="email"
name="email"
placeholder="Enter your email address"
required
/>
<AtSymbolIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
<div className="mt-4">
<label
className="mb-3 mt-5 block text-xs font-medium text-gray-900"
htmlFor="password"
>
Password
</label>
<div className="relative">
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
id="password"
type="password"
name="password"
placeholder="Enter password"
required
minLength={6}
/>
<KeyIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900" />
</div>
</div>
</div>
<LoginButton />
<div
className="flex h-8 items-end space-x-1"
aria-live="polite"
aria-atomic="true"
>
{errorMessage && (
<>
<ExclamationCircleIcon className="h-5 w-5 text-red-500" />
<p className="text-sm text-red-500">{errorMessage}</p>
</>
)}
</div>
</div>
</form>
);
}
function LoginButton() {
const { pending } = useFormStatus();
return (
<Button className="mt-4 w-full" aria-disabled={pending}>
Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
</Button>
);
}
Buat komponen atom button
dengan kode berikut:
src\app\components\atoms\button.tsx
import clsx from 'clsx';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}
export function Button({ children, className, ...rest }: ButtonProps) {
return (
<button
{...rest}
className={clsx(
'flex h-10 items-center rounded-lg bg-blue-500 px-4 text-sm font-medium text-white transition-colors hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500 active:bg-blue-600 aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
className,
)}
>
{children}
</button>
);
}
Pada praktikum ini, Anda akan menambahkan fungsi Logout, silakan lakukan langkah-langkah berikut ini.
sidenav
Untuk menambahkan fungsionalitas logout ke SideNav
, panggil fungsi signOut
dari auth.ts
di elemen form
Anda seperti kode berikut:
src\app\components\atoms\sidenav.tsx
import Link from 'next/link';
import NavLinks from './nav-links';
import AcmeLogo from './acme-logo';
import { PowerIcon } from '@heroicons/react/24/outline';
import { signOut } from '../../../../auth';
export default function SideNav() {
return (
<div className="flex h-full flex-col px-3 py-4 md:px-2">
<Link
className="mb-2 flex h-20 items-end justify-start rounded-md bg-blue-600 p-4 md:h-40"
href="/"
>
<div className="w-32 text-white md:w-40">
<AcmeLogo />
</div>
</Link>
<div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
<NavLinks />
<div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
<form
action={async () => {
'use server';
await signOut();
}}
>
<button className="flex h-[48px] w-full grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
<PowerIcon className="w-6" />
<div className="hidden md:block">Sign Out</div>
</button>
</form>
</div>
</div>
);
}
Selanjutnya buat komponen atom nav-links
agar kode di langkah 1 tidak error.
src\app\components\atoms\nav-links.tsx
'use client';
import {
UserGroupIcon,
HomeIcon,
DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
// Map of links to display in the side navigation.
// Depending on the size of the application, this would be stored in a database.
const links = [
{ name: 'Home', href: '/', icon: HomeIcon },
{
name: 'Invoices',
href: '/invoices',
icon: DocumentDuplicateIcon,
},
{ name: 'Customers', href: '/customers', icon: UserGroupIcon },
];
export default function NavLinks() {
const pathname = usePathname();
return (
<>
{links.map((link) => {
const LinkIcon = link.icon;
return (
<Link
key={link.name}
href={link.href}
className={clsx(
'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3',
{
'bg-sky-100 text-blue-600': pathname === link.href,
},
)}
>
<LinkIcon className="w-6" />
<p className="hidden md:block">{link.name}</p>
</Link>
);
})}
</>
);
}
acme-logo
Langkah berikutnya buatlah komponen atom acme-logo
dengan kode berikut:
src\app\components\atoms\acme-logo.tsx
import { GlobeAltIcon } from '@heroicons/react/24/outline';
import { lusitana } from './fonts';
export default function AcmeLogo() {
return (
<div
className={`${lusitana.className} flex flex-row items-center leading-none text-white`}
>
<GlobeAltIcon className="h-12 w-12 rotate-[15deg]" />
<p className="text-[44px] ">Acme</p>
</div>
);
}
Sekarang, cobalah. Anda harus dapat masuk dan keluar dari aplikasi Anda menggunakan kredensial berikut:
user@nextmail.com
123456
Tambahkan fungsi authentication
dan authorization
pada project kelompok UAS Anda !
Selamat, Anda telah berhasil menyelesaikan codelab ini. Semoga mendapatkan ilmu yang bermanfaat.