Comment implémenter l’authentification par jeton dans Next.js en utilisant les JWTs

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


VIDÉO MUO DU JOUR

FAIRE DÉFILER POUR CONTINUER AVEC LE CONTENU

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


Mise en place d’un projet Next.js

Pour commencer, installez Next.js en lançant la commande ci-dessous dans votre terminal.

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

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

Structure du dossier du projet Next.js 13 sur VS Code.

Ensuite, installez ces dépendances dans votre projet en utilisant npm, le gestionnaire de paquets Node.

 npm install jose universal-cookie 

Jose est un module JavaScript qui fournit un ensemble d’utilitaires pour travailler avec des jetons Web JSON tandis que la dépendance universal-cookie 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

Ouvrir le src/app créez un nouveau dossier et nommez-le login. Dans ce dossier, ajoutez un nouveau fichier page.js et inclure 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 rendra un simple formulaire de connexion sur le navigateur pour permettre aux utilisateurs d’entrer un nom d’utilisateur et un mot de passe.

Le client d’utilisation dans le code permet de s’assurer qu’une frontière est déclarée entre le code réservé au serveur et celui réservé au client dans le fichier app répertoire.

Dans ce cas, il est utilisé pour déclarer que le code de la page de connexion, en particulier l’élément handleSubmit n’est exécutée que sur le client ; dans le cas contraire, Next.js génère 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 la connexion, cette fonction capture les informations d’identification de l’utilisateur à partir du formulaire de connexion. Elle envoie ensuite une requête POST à un point de terminaison de l’API en transmettant les détails de l’utilisateur pour vérification.

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

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

À l’intérieur de l’élément src/app créez un nouveau dossier et nommez-le api. Dans ce dossier, ajoutez un nouveau fichier login/route.js et inclure 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 identifiants de connexion transmis dans les requêtes POST à l’aide de données fictives.

En cas de vérification réussie, elle génère un jeton JWT crypté associé aux détails de l’utilisateur authentifié. Enfin, elle envoie une réponse positive au client, en incluant le jeton dans les cookies de réponse ; dans le cas contraire, elle renvoie une réponse d’état d’échec.

Mise en œuvre de la logique de vérification du jeton

L’étape initiale de l’authentification par jeton consiste à générer le jeton après une procédure de connexion réussie. L’étape suivante consiste à mettre en œuvre la logique de vérification du jeton.

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

Dans le module src créer un nouveau répertoire libs/auth.js et inclure 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 décodée du jeton à la signature attendue, le serveur peut effectivement vérifier que le jeton fourni est valide et, en fin de compte, autoriser les demandes des utilisateurs.

Créer .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 une route protégée

Vous devez maintenant créer une route à laquelle seuls les utilisateurs authentifiés peuvent accéder. Pour ce faire, créez une nouvelle protected/page.js dans le fichier 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éer un nouveau dossier dans le répertoire src et le nommer crochets. A l’intérieur de ce dossier, ajoutez un nouveau useAuth/index.js et inclure 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 de l’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é dans l’élément auth état.

Ce faisant, il permet à d’autres composants d’accéder aux informations de l’utilisateur authentifié et de les utiliser. Cela est essentiel pour des scénarios tels que les mises à jour de l’interface utilisateur basées sur l’état d’authentification, les demandes d’API ultérieures ou le rendu d’un contenu différent en fonction des rôles de l’utilisateur.

Dans ce cas, vous utiliserez le crochet pour rendre un contenu différent sur l’élément home itinéraire basé sur l’état d’authentification d’un utilisateur.

Une approche alternative que vous pourriez considérer est de gérer la gestion de l’état en utilisant Redux Toolkit ou en employant un outil de gestion de l’é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 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="/login">Login</Link>
                )}
              </nav>
          </header>
  </>
}

Le code ci-dessus utilise l’élément useAuth pour gérer l’état de l’authentification. Ce faisant, il rend de manière conditionnelle une page d’accueil publique avec un lien vers l’application connexion lorsque l’utilisateur n’est pas authentifié, et affiche un paragraphe pour un utilisateur authentifié.

Ajout d’un intergiciel pour renforcer l’accès autorisé aux itinéraires protégés

Dans l’article src créer un nouveau répertoire middleware.js et ajoutez le code ci-dessous.

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

const AUTH_PAGES = ("/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: ("/login", "/protected/:path*") };

Ce code d’intergiciel agit comme un gardien. Il vérifie que lorsque les utilisateurs veulent 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 les accès non autorisés.

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

Laisser un commentaire