L’authentification par jetons représente une méthode largement adoptée pour préserver la sécurité des applications web et mobiles contre les incursions non autorisées. Au sein de l’écosystème Next.js, vous avez la possibilité d’exploiter les capacités d’authentification fournies par Next-auth.
Une autre approche consiste à élaborer un système d’authentification personnalisé reposant sur des jetons, en particulier les JSON Web Tokens (JWT). Cette voie offre un contrôle accru sur la logique d’authentification, vous permettant de configurer le système en fonction des exigences spécifiques de votre projet.
Mise en place d’un projet Next.js
Pour initier le processus, lancez l’installation de Next.js en exécutant la commande suivante dans votre terminal:
npx create-next-app@latest next-auth-jwt --experimental-app
Ce guide est conçu pour Next.js 13, qui incorpore le répertoire des applications.
Ensuite, veuillez installer ces dépendances dans votre projet en utilisant npm, le gestionnaire de paquets Node.
npm install jose universal-cookie
José est un module JavaScript qui offre un ensemble d’utilitaires pour manipuler les JSON Web Tokens, tandis que la dépendance universal-cookie facilite la gestion des cookies de navigateur dans les environnements côté client et serveur.
Conception de l’interface utilisateur du formulaire de connexion
Accédez au répertoire src/app, créez un nouveau dossier nommé login. À l’intérieur de ce dossier, ajoutez un fichier page.js et insérez le code ci-dessous:
"use client";
import { useRouter } from "next/navigation";export default function LoginPage() {
return (
<form onSubmit={handleSubmit}>
<label>
Nom d'utilisateur:
<input type="text" name="username" />
</label>
<label>
Mot de passe:
<input type="password" name="password" />
</label>
<button type="submit">Se connecter</button>
</form>
);
}
Ce code met en place un composant fonctionnel de page de connexion, affichant un formulaire simple pour la saisie du nom d’utilisateur et du mot de passe.
L’instruction « use client » dans le code établit une frontière claire entre le code exécuté exclusivement côté serveur et celui côté client dans le répertoire de l’application.
Dans ce contexte, elle indique que le code de la page de connexion, et notamment la fonction « handleSubmit », s’exécute uniquement côté client, évitant ainsi des erreurs potentielles de Next.js.
À présent, définissons le code de la fonction « handleSubmit ». Intégrez le code suivant au sein du composant fonctionnel:
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("Échec de la connexion");
}
};
Cette fonction gère la logique d’authentification, capturant les informations d’identification de l’utilisateur à partir du formulaire de connexion. Elle envoie ensuite une requête POST à un point de terminaison API pour la vérification des informations.
Si les informations d’identification sont valides, l’API renvoie un statut de succès dans la réponse. La fonction utilisera ensuite le routeur de Next.js pour rediriger l’utilisateur vers une URL spécifiée, en l’occurrence la route protégée.
Définition du point de terminaison de l’API de connexion
Dans le répertoire src/app, créez un nouveau dossier nommé api. À l’intérieur de ce dossier, créez un fichier login/route.js et insérez le code suivant:
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 });
}
L’objectif principal de cette API est de valider les informations d’identification transmises via les requêtes POST en utilisant des données simulées.
Après une vérification réussie, un jeton JWT crypté est généré, associé aux détails de l’utilisateur authentifié. Enfin, une réponse de succès est renvoyée au client, incluant le jeton dans les cookies de la réponse. Dans le cas contraire, une réponse d’échec est envoyée.
Implémentation de la logique de vérification des jetons
La première étape de l’authentification par jetons consiste à créer le jeton après une connexion réussie. La prochaine étape est l’implémentation de la logique de vérification.
En substance, la fonction « jwtVerify » du module Jose est utilisée pour valider les jetons JWT transmis avec les requêtes HTTP ultérieures.
Dans le répertoire src, créez un fichier libs/auth.js et insérez 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("La clé secrète JWT n'est pas configurée");
}
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 sert à signer et valider les jetons. En comparant la signature du jeton décodé avec la signature attendue, le serveur peut confirmer l’authenticité du jeton fourni et autoriser les requêtes des utilisateurs.
Créez un fichier .env dans le répertoire racine et ajoutez-y une clé secrète unique comme suit:
NEXT_PUBLIC_JWT_SECRET_KEY=votre_clé_secrète
Création d’une route protégée
Maintenant, il est nécessaire de créer une route accessible uniquement aux utilisateurs authentifiés. Pour cela, créez un fichier protected/page.js dans le répertoire src/app. Insérez-y le code suivant:
export default function ProtectedPage() {
return <h1>Page très protégée</h1>;
}
Création d’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 fichier useAuth/index.js et insérez le code suivant:
"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 informations de l’utilisateur authentifié dans l’état d’authentification.
Cela permet à d’autres composants d’accéder et d’exploiter les informations de l’utilisateur authentifié. C’est crucial 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 précis, le hook servira à afficher un contenu différent sur la route d’accueil selon l’état d’authentification de l’utilisateur.
Une autre approche à considérer est la gestion de l’état en utilisant Redux Toolkit ou un outil de gestion d’état tel que Jotai. Cette méthode assure un accès global à l’état d’authentification ou à tout autre état défini par les composants.
Modifiez le fichier app/page.js, supprimez le code de base Next.js et ajoutez le code ci-dessous:
"use client" ;import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
const auth = useAuth();
return <>
<h1>Page d'accueil publique</h1>
<header>
<nav>
{auth ? (
<p>Connecté</p>
) : (
<Link href="https://wilku.top/login">Se connecter</Link>
)}
</nav>
</header>
</>
}
Ce code utilise le hook « useAuth » pour gérer l’état d’authentification. Il affiche de manière conditionnelle une page d’accueil publique avec un lien vers la route de connexion si l’utilisateur n’est pas authentifié, ou un paragraphe si l’utilisateur est authentifié.
Ajout d’un middleware pour contrôler l’accès aux routes protégées
Dans le répertoire src, créez un fichier middleware.js et insérez 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 middleware agit comme une barrière de sécurité. Il vérifie que les utilisateurs souhaitant accéder à des pages protégées sont authentifiés, et les redirige vers la page de connexion si ce n’est pas le cas.
Sécurisation des applications Next.js
L’authentification par jetons est une approche de sécurité efficace. Cependant, ce n’est pas la seule stratégie pour protéger vos applications contre les accès non autorisés.
Pour renforcer la sécurité des applications face aux menaces cybernétiques, il est essentiel d’adopter une approche de sécurité globale, en traitant les failles et vulnérabilités potentielles de manière exhaustive afin d’assurer une protection complète.