Come implementare l’autenticazione token in Next.js utilizzando JWT

L’autenticazione dei token è una strategia popolare utilizzata per proteggere le applicazioni Web e mobili da accessi non autorizzati. In Next.js, puoi utilizzare le funzionalità di autenticazione fornite da Next-auth.

In alternativa, puoi scegliere di sviluppare un sistema di autenticazione personalizzato basato su token utilizzando JSON Web Token (JWT). In questo modo ti assicuri di avere un maggiore controllo sulla logica di autenticazione; essenzialmente, personalizzando il sistema per soddisfare esattamente i requisiti del tuo progetto.

Configura un progetto Next.js

Per iniziare, installa Next.js eseguendo il comando seguente sul tuo terminale.

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

Questa guida utilizzerà Next.js 13 che include la directory dell’app.

Successivamente, installa queste dipendenze nel tuo progetto utilizzando npm, il Node Package Manager.

 npm install jose universal-cookie 

José è un modulo JavaScript che fornisce una serie di utilità per lavorare con i token Web JSON mentre cookie-universale La dipendenza fornisce un modo semplice per lavorare con i cookie del browser sia negli ambienti lato client che in quelli lato server.

Creare l’interfaccia utente del modulo di accesso

Apri la directory src/app, crea una nuova cartella e chiamala login. All’interno di questa cartella, aggiungi un nuovo file page.js e includi il codice seguente.

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

Il codice sopra crea un componente funzionale della pagina di accesso che renderà un semplice modulo di accesso nel browser per consentire agli utenti di inserire un nome utente e una password.

L’istruzione use client nel codice garantisce che venga dichiarato un limite tra il codice solo server e quello solo client nella directory dell’app.

In questo caso viene utilizzato per dichiarare che il codice nella pagina di login, in particolare la funzione handleSubmit, viene eseguito solo sul client; in caso contrario, Next.js genererà un errore.

Ora definiamo il codice per la funzione handleSubmit. All’interno del componente funzionale, aggiungi il seguente codice.

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

Per gestire la logica di autenticazione del login, questa funzione cattura le credenziali dell’utente dal form di login. Invia quindi una richiesta POST a un endpoint API trasmettendo i dettagli dell’utente per la verifica.

Se le credenziali sono valide, indicando che il processo di accesso ha avuto esito positivo: l’API restituisce uno stato di successo nella risposta. La funzione del gestore utilizzerà quindi il router di Next.js per indirizzare l’utente verso un URL specificato, in questo caso il percorso protetto.

Definire l’endpoint API di accesso

All’interno della directory src/app, crea una nuova cartella e chiamala api. All’interno di questa cartella, aggiungi un nuovo file login/route.js e includi il codice seguente.

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

Il compito principale di questa API è verificare le credenziali di accesso passate nelle richieste POST utilizzando dati fittizi.

Una volta verificata con successo, genera un token JWT crittografato associato ai dettagli dell’utente autenticato. Infine, invia una risposta positiva al client, includendo il token nei cookie di risposta; in caso contrario, restituisce una risposta sullo stato di errore.

Implementare la logica di verifica dei token

Il passaggio iniziale nell’autenticazione del token è la generazione del token dopo un processo di accesso riuscito. Il passaggio successivo consiste nell’implementare la logica per la verifica del token.

In sostanza, utilizzerai la funzione jwtVerify fornita dal modulo Jose per verificare i token JWT passati con le successive richieste HTTP.

Nella directory src, crea un nuovo file libs/auth.js e includi il codice seguente.

 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 chiave segreta viene utilizzata per firmare e verificare i token. Confrontando la firma del token decodificato con la firma prevista, il server può effettivamente verificare che il token fornito sia valido e, infine, autorizzare le richieste degli utenti.

Crea il file .env nella directory root e aggiungi una chiave segreta univoca come segue:

 NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key 

Crea un percorso protetto

Ora devi creare un percorso a cui solo gli utenti autenticati possono accedere. Per fare ciò, crea un nuovo file protected/page.js nella directory src/app. All’interno di questo file, aggiungi il seguente codice.

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

Crea un hook per gestire lo stato di autenticazione

Crea una nuova cartella nella directory src e chiamala hooks. All’interno di questa cartella aggiungi un nuovo file useAuth/index.js e includi il codice seguente.

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

Questo hook gestisce lo stato di autenticazione sul lato client. Recupera e verifica la validità del token JWT presente nei cookie utilizzando la funzione verifyJwtToken, quindi imposta i dettagli dell’utente autenticato sullo stato di autenticazione.

In questo modo, consente ad altri componenti di accedere e utilizzare le informazioni dell’utente autenticato. Ciò è essenziale per scenari come la realizzazione di aggiornamenti dell’interfaccia utente in base allo stato di autenticazione, l’esecuzione di richieste API successive o il rendering di contenuti diversi in base ai ruoli utente.

In questo caso, utilizzerai l’hook per eseguire il rendering di contenuti diversi sulla home route in base allo stato di autenticazione di un utente.

Un approccio alternativo che potresti prendere in considerazione è gestire la gestione dello stato utilizzando Redux Toolkit o impiegando uno strumento di gestione dello stato come Jotai. Questo approccio garantisce che i componenti possano ottenere l’accesso globale allo stato di autenticazione o a qualsiasi altro stato definito.

Vai avanti e apri il file app/page.js, elimina il codice Next.js boilerplate e aggiungi il seguente codice.

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

Il codice precedente utilizza l’hook useAuth per gestire lo stato di autenticazione. In tal modo, rende condizionatamente una home page pubblica con un collegamento al percorso della pagina di accesso quando l’utente non è autenticato e visualizza un paragrafo per un utente autenticato.

Aggiungi un middleware per imporre l’accesso autorizzato ai percorsi protetti

Nella directory src, crea un nuovo file middleware.js e aggiungi il codice seguente.

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

Questo codice middleware funge da guardia. Controlla per garantire che quando gli utenti desiderano accedere alle pagine protette, siano autenticati e autorizzati ad accedere ai percorsi, oltre a reindirizzare gli utenti non autorizzati alla pagina di accesso.

Protezione delle applicazioni Next.js

L’autenticazione tramite token è un meccanismo di sicurezza efficace. Tuttavia, non è l’unica strategia disponibile per proteggere le tue applicazioni da accessi non autorizzati.

Per rafforzare le applicazioni rispetto al panorama dinamico della sicurezza informatica, è importante adottare un approccio alla sicurezza completo che affronti in modo olistico potenziali lacune e vulnerabilità della sicurezza per garantire una protezione completa.