Jak implementovat ověřování tokenů v Next.js pomocí JWT

Photo of author

By etechblogcz

Autentizace pomocí tokenů se stala oblíbeným postupem pro zabezpečení webových a mobilních aplikací proti neoprávněnému přístupu. V prostředí Next.js máte možnost využít autentizační funkce, které poskytuje knihovna Next-auth.

Alternativně můžete implementovat vlastní ověřovací systém založený na tokenech s využitím JSON Web Tokenů (JWT). Tento přístup vám poskytne větší kontrolu nad logikou autentizace a umožní přizpůsobit systém přesně dle potřeb vašeho projektu.

Inicializace projektu Next.js

Začněte instalací Next.js pomocí následujícího příkazu ve vašem terminálu.

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

Tento návod využívá Next.js 13, který obsahuje adresář aplikace.

Následně nainstalujte potřebné závislosti do vašeho projektu pomocí npm, správce balíčků uzlů.

 npm install jose universal-cookie 

Jose je javascriptový modul, který nabízí nástroje pro práci s JSON Web Tokeny, zatímco universal-cookie poskytuje jednoduché řešení pro práci se soubory cookie prohlížeče jak na straně klienta, tak na straně serveru.

Vytvoření uživatelského rozhraní přihlašovacího formuláře

Přejděte do adresáře src/app, vytvořte novou složku s názvem login a do této složky přidejte soubor page.js s níže uvedeným kódem.

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

export default function LoginPage() {
  return (
    <form onSubmit={handleSubmit}>
      <label>
        Uživatelské jméno:
        <input type="text" name="username" />
      </label>
      <label>
        Heslo:
        <input type="password" name="password" />
      </label>
      <button type="submit">Přihlásit</button>
    </form>
  );
}

Tento kód vytvoří funkční komponentu přihlašovací stránky, která zobrazí jednoduchý přihlašovací formulář, kde uživatelé mohou zadat své uživatelské jméno a heslo.

Příkaz `use client` v kódu definuje hranici mezi kódem určeným výhradně pro server a kódem pro klienta v adresáři aplikace.

V tomto kontextu se používá k označení, že kód přihlašovací stránky, konkrétně funkce `handleSubmit`, se má provádět pouze na straně klienta. Jinak by Next.js vyvolal chybu.

Nyní implementujeme kód pro funkci `handleSubmit`. Přidejte následující kód do funkční komponenty.

 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("Přihlášení se nezdařilo");
    }
 };

Tato funkce zajišťuje logiku přihlášení. Získává přihlašovací údaje z formuláře a odesílá POST požadavek na API endpoint pro ověření.

V případě platných údajů – což značí úspěšné přihlášení – API endpoint vrátí úspěch v odpovědi. Funkce `handleSubmit` následně použije Next.js router k přesměrování uživatele na zadanou adresu, v tomto případě na chráněnou stránku.

Definování API endpointu pro přihlášení

Vytvořte v adresáři `src/app` novou složku `api`. V této složce vytvořte další složku s názvem `login` a v ní soubor `route.js` s následujícím obsahem.

 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 });
}

Tento API endpoint primárně slouží k ověření přihlašovacích údajů zaslaných v POST požadavku, pro zjednodušení se používají statická data.

Při úspěšném ověření vygeneruje šifrovaný JWT token obsahující informace o ověřeném uživateli. Následně odesílá klientovi odpověď s úspěchem, včetně tokenu v hlavičce cookie, jinak vrátí stavový kód neúspěchu.

Implementace logiky ověření tokenu

Prvním krokem při autentizaci tokenem je jeho vygenerování po úspěšném přihlášení. Druhým krokem je implementace logiky pro ověření tokenu.

Pro ověření JWT tokenů předávaných v HTTP požadavcích použijeme funkci `jwtVerify` z modulu Jose.

Vytvořte nový soubor `libs/auth.js` v adresáři `src` a přidejte do něj následující kód.

 import { jwtVerify } from "jose";

export function getJwtSecretKey() {
  const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
  if (!secret) {
    throw new Error("Tajný klíč JWT nebyl nalezen");
  }
  return new TextEncoder().encode(secret);
}

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

Tajný klíč se využívá pro podepisování a ověřování tokenů. Porovnáním dešifrovaného podpisu tokenu s očekávaným podpisem může server ověřit platnost tokenu a autorizovat uživatelské požadavky.

Vytvořte soubor `.env` v kořenovém adresáři a přidejte do něj jedinečný tajný klíč v následujícím formátu:

 NEXT_PUBLIC_JWT_SECRET_KEY=vas_tajny_klic 

Vytvoření chráněné cesty

Nyní musíte vytvořit cestu, která bude přístupná pouze ověřeným uživatelům. Vytvořte soubor `protected/page.js` v adresáři `src/app` a vložte do něj následující kód.

 export default function ProtectedPage() {
    return <h1>Chráněná stránka</h1>;
  }

Vytvoření Hooku pro správu stavu autentizace

Vytvořte novou složku `hooks` v adresáři `src`. V této složce vytvořte soubor `useAuth/index.js` a přidejte níže uvedený kód.

 "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;
}

Tento hook spravuje stav autentizace na straně klienta. Načte a ověří platnost JWT tokenu z cookies pomocí funkce `verifyJwtToken` a následně uloží detaily ověřeného uživatele do stavu `auth`.

Tím umožní ostatním komponentám přístup k informacím o ověřeném uživateli. To je užitečné v situacích, kdy potřebujete aktualizovat uživatelské rozhraní na základě stavu ověření, provádět následné API požadavky, nebo vykreslovat různý obsah na základě uživatelských rolí.

V tomto případě se hook použije pro vykreslování odlišného obsahu na domovské stránce v závislosti na stavu ověření uživatele.

Alternativním přístupem by mohlo být použití Redux Toolkit, nebo jiného nástroje pro správu stavu, jako je Jotai. Tento přístup zajistí, že komponenty mohou získat globální přístup ke stavu autentizace nebo jinému definovanému stavu.

Otevřete soubor `app/page.js`, smažte výchozí kód Next.js a vložte následující kód.

 "use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
  const auth = useAuth();
  return <>
           <h1>Veřejná domovská stránka</h1>
           <header>
              <nav>
                {auth ? (
                   <p>Jste přihlášen</p>
                ) : (
                  <Link href="https://wilku.top/login">Přihlásit se</Link>
                )}
              </nav>
          </header>
  </>
}

Výše uvedený kód používá `useAuth` hook pro správu stavu ověření. Podmíněně zobrazí veřejnou domovskou stránku s odkazem na přihlašovací stránku, pokud uživatel není ověřen, jinak zobrazí hlášku pro ověřeného uživatele.

Přidání middlewaru pro vynucení autorizovaného přístupu k chráněným cestám

V adresáři `src` vytvořte soubor `middleware.js` a vložte do něj následující kód.

 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*"] };

Tento middleware funguje jako strážce přístupu. Kontroluje, zda jsou uživatelé, kteří chtějí přistoupit k chráněným stránkám, autentizovaní a autorizovaní. Neoprávněné uživatele přesměruje na přihlašovací stránku.

Zabezpečení aplikací Next.js

Autentizace tokenem je efektivní bezpečnostní mechanismus. Nicméně, není to jediná strategie pro ochranu aplikací před neoprávněným přístupem.

Pro zvýšení odolnosti aplikací proti dynamickému kybernetickému prostředí je důležité používat komplexní bezpečnostní přístup, který holisticky řeší potenciální mezery a zranitelnosti a tím zajistí důkladnou ochranu.