Як реалізувати автентифікацію маркерів у Next.js за допомогою JWT

Автентифікація за маркерами — популярна стратегія, яка використовується для захисту веб- і мобільних програм від несанкціонованого доступу. У Next.js ви можете використовувати функції автентифікації, які надає Next-auth.

Крім того, ви можете розробити спеціальну систему автентифікації на основі маркерів за допомогою веб-токенів JSON (JWT). Роблячи це, ви забезпечуєте більше контролю над логікою автентифікації; по суті, налаштування системи відповідно до вимог вашого проекту.

Налаштуйте проект Next.js

Щоб почати, інсталюйте Next.js, виконавши наведену нижче команду на своєму терміналі.

 npx create-next-app@latest next-auth-jwt --experimental-app 

У цьому посібнику буде використано Next.js 13, який містить каталог програм.

Далі встановіть ці залежності у свій проект за допомогою npm, Node Package Manager.

 npm install jose universal-cookie 

Хосе — це модуль JavaScript, який надає набір утиліт для роботи з веб-токенами JSON, а універсальне печиво dependency забезпечує простий спосіб роботи з файлами cookie браузера як на стороні клієнта, так і на стороні сервера.

Створіть інтерфейс користувача форми входу

Відкрийте каталог src/app, створіть нову папку та назвіть її login. У цю папку додайте новий файл page.js і додайте наведений нижче код.

 "use client";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Username:
        <input type="text" name="username" />
      </label>
      <label>
        Password:
        <input type="password" name="password" />
      </label>
      <button type="submit">Login</button>
    </form>
  );
}

Наведений вище код створює функціональний компонент сторінки входу, який відображатиме просту форму входу в браузері, щоб користувачі могли вводити ім’я користувача та пароль.

Інструкція use client у коді гарантує, що в каталозі програми оголошено межу між кодом лише для сервера та лише для клієнта.

У цьому випадку він використовується для оголошення того, що код на сторінці входу, зокрема функція handleSubmit, виконується лише на клієнті; інакше Next.js видасть помилку.

Тепер давайте визначимо код для функції handleSubmit. У функціональний компонент додайте наступний код.

 const router = useRouter();

const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    const username = formData.get("username");
    const password = formData.get("password");
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify({ username, password }),
    });
    const { success } = await res.json();
    if (success) {
      router.push("/protected");
      router.refresh();
    } else {
      alert("Login failed");
    }
 };

Щоб керувати логікою автентифікації входу, ця функція фіксує облікові дані користувача з форми входу. Потім він надсилає запит POST до кінцевої точки API, передаючи дані користувача для перевірки.

Якщо облікові дані дійсні, що вказує на те, що процес входу був успішним, API повертає у відповідь статус успіху. Потім функція обробника використовуватиме маршрутизатор Next.js для переходу користувача до вказаної URL-адреси, у цьому випадку захищеного маршруту.

Визначте кінцеву точку API входу

У каталозі src/app створіть нову папку та назвіть її api. У цю папку додайте новий файл login/route.js і додайте наведений нижче код.

 import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";

export async function POST(request) {
  const body = await request.json();
  if (body.username === "admin" && body.password === "admin") {
    const token = await new SignJWT({
      username: body.username,
    })
      .setProtectedHeader({ alg: "HS256" })
      .setIssuedAt()
      .setExpirationTime("30s")
      .sign(getJwtSecretKey());
    const response = NextResponse.json(
      { success: true },
      { status: 200, headers: { "content-type": "application/json" } }
    );
    response.cookies.set({
      name: "token",
      value: token,
      path: "https://www.makeuseof.com/",
    });
    return response;
  }
  return NextResponse.json({ success: false });
}

Основним завданням цього API є перевірка облікових даних для входу, переданих у запитах POST, за допомогою фіктивних даних.

Після успішної перевірки він генерує зашифрований маркер JWT, пов’язаний із деталями автентифікованого користувача. Нарешті, він надсилає успішну відповідь клієнту, включаючи маркер у відповідь cookies; інакше він повертає відповідь про статус помилки.

Впровадити логіку перевірки маркерів

Початковим кроком автентифікації маркера є генерація маркера після успішного процесу входу. Наступним кроком є ​​впровадження логіки перевірки маркера.

По суті, ви будете використовувати функцію jwtVerify, надану модулем Jose, щоб перевірити маркери JWT, передані з наступними запитами HTTP.

У каталозі src створіть новий файл libs/auth.js і додайте наведений нижче код.

 import { jwtVerify } from "jose";

export function getJwtSecretKey() {
  const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
  if (!secret) {
    throw new Error("JWT Secret key is not matched");
  }
  return new TextEncoder().encode(secret);
}

export async function verifyJwtToken(token) {
  try {
    const { payload } = await jwtVerify(token, getJwtSecretKey());
    return payload;
  } catch (error) {
    return null;
  }
}

Секретний ключ використовується для підписання та перевірки токенів. Порівнюючи декодований підпис маркера з очікуваним підписом, сервер може ефективно перевірити, чи наданий маркер є дійсним, і, зрештою, авторизувати запити користувачів.

Створіть файл .env у кореневому каталозі та додайте унікальний секретний ключ таким чином:

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

Створіть захищений маршрут

Тепер вам потрібно створити маршрут, до якого зможуть отримати доступ лише автентифіковані користувачі. Для цього створіть новий файл protected/page.js у каталозі src/app. У цей файл додайте наступний код.

 export default function ProtectedPage() {
    return <h1>Very protected page</h1>;
  }

Створіть хук для керування станом автентифікації

Створіть нову папку в каталозі src і назвіть її hooks. У цю папку додайте новий файл useAuth/index.js і додайте наведений нижче код.

 "use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";

export function useAuth() {
  const [auth, setAuth] = React.useState(null);

  const getVerifiedtoken = async () => {
    const cookies = new Cookies();
    const token = cookies.get("token") ?? null;
    const verifiedToken = await verifyJwtToken(token);
    setAuth(verifiedToken);
  };
  React.useEffect(() => {
    getVerifiedtoken();
  }, []);
  return auth;
}

Цей хук керує станом автентифікації на стороні клієнта. Він отримує та перевіряє дійсність маркера JWT, наявного в файлах cookie, за допомогою функції verifyJwtToken, а потім встановлює дані автентифікованого користувача в стан авторизації.

Таким чином, він дозволяє іншим компонентам отримувати доступ і використовувати інформацію автентифікованого користувача. Це важливо для таких сценаріїв, як оновлення інтерфейсу користувача на основі статусу автентифікації, виконання наступних запитів API або рендеринг різного вмісту на основі ролей користувача.

У цьому випадку ви використовуватимете хук для відтворення різного вмісту на домашньому маршруті на основі стану автентифікації користувача.

Альтернативним підходом, який ви можете розглянути, є керування станом за допомогою Redux Toolkit або використання інструменту керування станом, як-от Jotai. Цей підхід гарантує, що компоненти можуть отримати глобальний доступ до стану автентифікації або будь-якого іншого визначеного стану.

Відкрийте файл app/page.js, видаліть шаблонний код Next.js і додайте наступний код.

 "use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
           <h1>Public Home Page</h1>
           <header>
              <nav>
                {auth ? (
                   <p>logged in</p>
                ) : (
                  <Link href="https://wilku.top/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

Наведений вище код використовує хук useAuth для керування станом автентифікації. При цьому він умовно відображає загальнодоступну домашню сторінку з посиланням на маршрут сторінки входу, коли користувач не автентифікований, і відображає абзац для автентифікованого користувача.

Додайте проміжне програмне забезпечення для забезпечення авторизованого доступу до захищених маршрутів

У каталозі src створіть новий файл middleware.js і додайте наведений нижче код.

 import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";

const AUTH_PAGES = ["https://wilku.top/login"];

const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));

export async function middleware(request) {

  const { url, nextUrl, cookies } = request;
  const { value: token } = cookies.get("token") ?? { value: null };
  const hasVerifiedToken = token && (await verifyJwtToken(token));
  const isAuthPageRequested = isAuthPages(nextUrl.pathname);

  if (isAuthPageRequested) {
    if (!hasVerifiedToken) {
      const response = NextResponse.next();
      response.cookies.delete("token");
      return response;
    }
    const response = NextResponse.redirect(new URL(`/`, url));
    return response;
  }

  if (!hasVerifiedToken) {
    const searchParams = new URLSearchParams(nextUrl.searchParams);
    searchParams.set("next", nextUrl.pathname);
    const response = NextResponse.redirect(
      new URL(`/login?${searchParams}`, url)
    );
    response.cookies.delete("token");
    return response;
  }

  return NextResponse.next();

}
export const config = { matcher: ["https://wilku.top/login", "/protected/:path*"] };

Цей код проміжного програмного забезпечення діє як охоронець. Він перевіряє, щоб користувачі, які хочуть отримати доступ до захищених сторінок, пройшли автентифікацію та авторизувалися для доступу до маршрутів, а також перенаправляли неавторизованих користувачів на сторінку входу.

Захист додатків Next.js

Аутентифікація за маркерами є ефективним механізмом безпеки. Однак це не єдина доступна стратегія захисту ваших програм від несанкціонованого доступу.

Щоб захистити програми від динамічного ландшафту кібербезпеки, важливо прийняти комплексний підхід до безпеки, який цілісно усуває потенційні лазівки та вразливості безпеки, щоб гарантувати повний захист.