Comment implémenter l’authentification par jeton dans Next.js à l’aide de JWT

L’authentification par jeton est une stratégie populaire utilisée pour protéger les applications Web et mobiles contre tout accès non autorisé. Dans Next.js, vous pouvez utiliser les fonctionnalités d’authentification fournies par Next-auth.

Vous pouvez également choisir de développer un système d’authentification personnalisé basé sur des jetons à l’aide de jetons Web JSON (JWT). Ce faisant, vous vous assurez d’avoir plus de contrôle sur la logique d’authentification ; essentiellement, personnaliser le système pour qu’il corresponde précisément aux exigences de votre projet.

Configurer un projet Next.js

Pour commencer, installez Next.js en exécutant la commande ci-dessous sur votre terminal.

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

Ce guide utilisera Next.js 13 qui inclut le répertoire des applications.

Ensuite, installez ces dépendances dans votre projet à l’aide de npm, le Node Package Manager.

 npm install jose universal-cookie 

José est un module JavaScript qui fournit un ensemble d’utilitaires permettant de travailler avec les jetons Web JSON pendant que le cookie-universel La dépendance fournit un moyen simple de travailler avec les cookies du navigateur dans les environnements côté client et côté serveur.

Créer l’interface utilisateur du formulaire de connexion

Ouvrez le répertoire src/app, créez un nouveau dossier et nommez-le login. Dans ce dossier, ajoutez un nouveau fichier page.js et incluez le code ci-dessous.

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

Le code ci-dessus crée un composant fonctionnel de page de connexion qui affichera un simple formulaire de connexion sur le navigateur pour permettre aux utilisateurs de saisir un nom d’utilisateur et un mot de passe.

L’instruction use client dans le code garantit qu’une limite est déclarée entre le code serveur uniquement et le code client uniquement dans le répertoire de l’application.

Dans ce cas, il est utilisé pour déclarer que le code de la page de connexion, en particulier la fonction handleSubmit, n’est exécuté que sur le client ; sinon, Next.js générera une erreur.

Maintenant, définissons le code de la fonction handleSubmit. À l’intérieur du composant fonctionnel, ajoutez le code suivant.

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

Pour gérer la logique d’authentification de connexion, cette fonction capture les informations d’identification de l’utilisateur à partir du formulaire de connexion. Il envoie ensuite une requête POST à ​​un point de terminaison d’API transmettant les détails de l’utilisateur pour vérification.

Si les informations d’identification sont valides, indiquant que le processus de connexion a réussi, l’API renvoie un statut de réussite dans la réponse. La fonction de gestionnaire utilisera ensuite le routeur de Next.js pour diriger l’utilisateur vers une URL spécifiée, dans ce cas, la route protégée.

Définir le point de terminaison de l’API de connexion

Dans le répertoire src/app, créez un nouveau dossier et nommez-le api. Dans ce dossier, ajoutez un nouveau fichier login/route.js et incluez le code ci-dessous.

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

La tâche principale de cette API est de vérifier les informations de connexion transmises dans les requêtes POST à ​​l’aide de données fictives.

Une fois la vérification réussie, il génère un jeton JWT crypté associé aux détails de l’utilisateur authentifié. Enfin, il envoie une réponse réussie au client, incluant le jeton dans les cookies de réponse ; sinon, il renvoie une réponse d’état d’échec.

Implémenter la logique de vérification des jetons

La première étape de l’authentification par jeton consiste à générer le jeton après un processus de connexion réussi. L’étape suivante consiste à implémenter la logique de vérification des jetons.

Essentiellement, vous utiliserez la fonction jwtVerify fournie par le module Jose pour vérifier les jetons JWT transmis avec les requêtes HTTP suivantes.

Dans le répertoire src, créez un nouveau fichier libs/auth.js et incluez le code ci-dessous.

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

La clé secrète est utilisée pour signer et vérifier les jetons. En comparant la signature du jeton décodée à la signature attendue, le serveur peut vérifier efficacement que le jeton fourni est valide et, finalement, autoriser les demandes des utilisateurs.

Créez un fichier .env dans le répertoire racine et ajoutez une clé secrète unique comme suit :

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

Créer un itinéraire protégé

Maintenant, vous devez créer un itinéraire auquel seuls les utilisateurs authentifiés peuvent accéder. Pour ce faire, créez un nouveau fichier protected/page.js dans le répertoire src/app. Dans ce fichier, ajoutez le code suivant.

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

Créer un hook pour gérer l’état d’authentification

Créez un nouveau dossier dans le répertoire src et nommez-le hooks. Dans ce dossier, ajoutez un nouveau fichier useAuth/index.js et incluez le code ci-dessous.

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

Ce hook gère l’état d’authentification côté client. Il récupère et vérifie la validité du jeton JWT présent dans les cookies à l’aide de la fonction verifyJwtToken, puis définit les détails de l’utilisateur authentifié sur l’état d’authentification.

Ce faisant, il permet à d’autres composants d’accéder et d’utiliser les informations de l’utilisateur authentifié. Ceci est essentiel pour des scénarios tels que la mise à jour de l’interface utilisateur en fonction du statut d’authentification, l’envoi de requêtes API ultérieures ou le rendu de contenu différent en fonction des rôles d’utilisateur.

Dans ce cas, vous utiliserez le hook pour afficher différents contenus sur la route d’accueil en fonction de l’état d’authentification d’un utilisateur.

Une approche alternative que vous pourriez envisager consiste à gérer la gestion de l’état à l’aide de Redux Toolkit ou à utiliser un outil de gestion d’état comme Jotai. Cette approche garantit que les composants peuvent obtenir un accès global à l’état d’authentification ou à tout autre état défini.

Allez-y et ouvrez le fichier app/page.js, supprimez le code passe-partout Next.js et ajoutez le code suivant.

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

Le code ci-dessus utilise le hook useAuth pour gérer l’état d’authentification. Ce faisant, il restitue sous condition une page d’accueil publique avec un lien vers la route de la page de connexion lorsque l’utilisateur n’est pas authentifié, et affiche un paragraphe pour un utilisateur authentifié.

Ajoutez un middleware pour appliquer l’accès autorisé aux routes protégées

Dans le répertoire src, créez un nouveau fichier middleware.js et ajoutez le code ci-dessous.

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

Ce code middleware agit comme une garde. Il vérifie que lorsque les utilisateurs souhaitent accéder à des pages protégées, ils sont authentifiés et autorisés à accéder aux itinéraires, en plus de rediriger les utilisateurs non autorisés vers la page de connexion.

Sécuriser les applications Next.js

L’authentification par jeton est un mécanisme de sécurité efficace. Cependant, ce n’est pas la seule stratégie disponible pour protéger vos applications contre tout accès non autorisé.

Pour renforcer les applications face au paysage dynamique de la cybersécurité, il est important d’adopter une approche de sécurité globale qui corrige de manière globale les failles de sécurité et les vulnérabilités potentielles afin de garantir une protection complète.