Comment implémenter l'authentification dans Next.js

Comprendre l'authentification est crucial pour protéger les données de votre application. Cette page vous guidera à travers les fonctionnalités de React et Next.js à utiliser pour implémenter l'authentification.

Avant de commencer, il est utile de décomposer le processus en trois concepts :

  1. Authentification : Vérifie si l'utilisateur est bien celui qu'il prétend être. Cela nécessite que l'utilisateur prouve son identité avec quelque chose qu'il connaît, comme un nom d'utilisateur et un mot de passe.
  2. Gestion de session : Suit l'état d'authentification de l'utilisateur à travers les requêtes.
  3. Autorisation : Détermine quelles routes et données l'utilisateur peut accéder.

Ce diagramme montre le flux d'authentification utilisant les fonctionnalités de React et Next.js :

Diagramme montrant le flux d'authentification avec les fonctionnalités de React et Next.js

Les exemples de cette page parcourent une authentification basique avec nom d'utilisateur et mot de passe à des fins éducatives. Bien que vous puissiez implémenter une solution d'authentification personnalisée, pour une sécurité accrue et une simplicité, nous recommandons d'utiliser une bibliothèque d'authentification. Celles-ci offrent des solutions intégrées pour l'authentification, la gestion de session et l'autorisation, ainsi que des fonctionnalités supplémentaires comme les connexions sociales, l'authentification multi-facteurs et le contrôle d'accès basé sur les rôles. Vous trouverez une liste dans la section Bibliothèques d'authentification.

Authentification

Fonctionnalité d'inscription et de connexion

Vous pouvez utiliser l'élément <form> avec les Actions Serveur de React et useActionState pour capturer les identifiants utilisateur, valider les champs du formulaire et appeler l'API ou la base de données de votre fournisseur d'authentification.

Comme les Actions Serveur s'exécutent toujours sur le serveur, elles fournissent un environnement sécurisé pour gérer la logique d'authentification.

Voici les étapes pour implémenter la fonctionnalité d'inscription/connexion :

1. Capturer les identifiants utilisateur

Pour capturer les identifiants utilisateur, créez un formulaire qui invoque une Action Serveur lors de la soumission. Par exemple, un formulaire d'inscription qui accepte le nom, l'email et le mot de passe de l'utilisateur :

import { signup } from '@/app/actions/auth'

export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">Nom</label>
        <input id="name" name="name" placeholder="Nom" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" type="email" placeholder="Email" />
      </div>
      <div>
        <label htmlFor="password">Mot de passe</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">S'inscrire</button>
    </form>
  )
}

2. Valider les champs du formulaire côté serveur

Utilisez l'Action Serveur pour valider les champs du formulaire côté serveur. Si votre fournisseur d'authentification ne fournit pas de validation de formulaire, vous pouvez utiliser une bibliothèque de validation de schéma comme Zod ou Yup.

En utilisant Zod comme exemple, vous pouvez définir un schéma de formulaire avec des messages d'erreur appropriés :

import { z } from 'zod'

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'Le nom doit contenir au moins 2 caractères.' })
    .trim(),
  email: z.string().email({ message: 'Veuillez entrer un email valide.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Doit contenir au moins 8 caractères' })
    .regex(/[a-zA-Z]/, { message: 'Doit contenir au moins une lettre.' })
    .regex(/[0-9]/, { message: 'Doit contenir au moins un chiffre.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Doit contenir au moins un caractère spécial.',
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

Pour éviter des appels inutiles à l'API ou à la base de données de votre fournisseur d'authentification, vous pouvez return prématurément dans l'Action Serveur si des champs du formulaire ne correspondent pas au schéma défini.

import { SignupFormSchema, FormState } from '@/app/lib/definitions'

export async function signup(state: FormState, formData: FormData) {
  // Valider les champs du formulaire
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })

  // Si des champs sont invalides, retourner prématurément
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Appeler le fournisseur ou la base de données pour créer un utilisateur...
}

De retour dans votre <SignupForm />, vous pouvez utiliser le hook useActionState de React pour afficher les erreurs de validation pendant la soumission du formulaire :

'use client'

import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'

export default function SignupForm() {
  const [state, action, pending] = useActionState(signup, undefined)

  return (
    <form action={action}>
      <div>
        <label htmlFor="name">Nom</label>
        <input id="name" name="name" placeholder="Nom" />
      </div>
      {state?.errors?.name && <p>{state.errors.name}</p>}

      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" placeholder="Email" />
      </div>
      {state?.errors?.email && <p>{state.errors.email}</p>}

      <div>
        <label htmlFor="password">Mot de passe</label>
        <input id="password" name="password" type="password" />
      </div>
      {state?.errors?.password && (
        <div>
          <p>Le mot de passe doit :</p>
          <ul>
            {state.errors.password.map((error) => (
              <li key={error}>- {error}</li>
            ))}
          </ul>
        </div>
      )}
      <button disabled={pending} type="submit">
        S'inscrire
      </button>
    </form>
  )
}

Bon à savoir :

  • Dans React 19, useFormStatus inclut des clés supplémentaires sur l'objet retourné, comme data, method et action. Si vous n'utilisez pas React 19, seule la clé pending est disponible.
  • Avant de muter des données, vous devriez toujours vous assurer qu'un utilisateur est également autorisé à effectuer l'action. Voir Authentification et Autorisation.

3. Créer un utilisateur ou vérifier ses identifiants

Après avoir validé les champs du formulaire, vous pouvez créer un nouveau compte utilisateur ou vérifier si l'utilisateur existe en appelant l'API ou la base de données de votre fournisseur d'authentification.

Reprenons l'exemple précédent :

export async function signup(state: FormState, formData: FormData) {
  // 1. Valider les champs du formulaire
  // ...

  // 2. Préparer les données pour l'insertion dans la base de données
  const { name, email, password } = validatedFields.data
  // Par exemple, hacher le mot de passe de l'utilisateur avant de le stocker
  const hashedPassword = await bcrypt.hash(password, 10)

  // 3. Insérer l'utilisateur dans la base de données ou appeler l'API d'une bibliothèque d'authentification
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })

  const user = data[0]

  if (!user) {
    return {
      message: 'Une erreur est survenue lors de la création de votre compte.',
    }
  }

  // À FAIRE :
  // 4. Créer une session utilisateur
  // 5. Rediriger l'utilisateur
}

Après avoir créé avec succès le compte utilisateur ou vérifié ses identifiants, vous pouvez créer une session pour gérer l'état d'authentification de l'utilisateur. Selon votre stratégie de gestion des sessions, celle-ci peut être stockée dans un cookie, une base de données, ou les deux. Passez à la section Gestion des sessions pour en savoir plus.

Conseils :

  • L'exemple ci-dessus est détaillé car il décompose les étapes d'authentification à des fins pédagogiques. Cela montre qu'implémenter votre propre solution sécurisée peut rapidement devenir complexe. Envisagez d'utiliser une bibliothèque d'authentification pour simplifier le processus.
  • Pour améliorer l'expérience utilisateur, vous pouvez vérifier les emails ou noms d'utilisateur en double plus tôt dans le flux d'inscription. Par exemple, lorsque l'utilisateur saisit un nom d'utilisateur ou lorsque le champ perd le focus. Cela peut éviter des soumissions inutiles du formulaire et fournir un retour immédiat à l'utilisateur. Vous pouvez limiter la fréquence de ces vérifications avec des bibliothèques comme use-debounce.

Gestion des sessions

La gestion des sessions garantit que l'état authentifié de l'utilisateur est conservé entre les requêtes. Elle implique la création, le stockage, le rafraîchissement et la suppression des sessions ou des jetons.

Il existe deux types de sessions :

  1. Sans état (Stateless) : Les données de session (ou un jeton) sont stockées dans les cookies du navigateur. Le cookie est envoyé avec chaque requête, permettant de vérifier la session côté serveur. Cette méthode est plus simple, mais peut être moins sécurisée si mal implémentée.
  2. Base de données (Database) : Les données de session sont stockées dans une base de données, le navigateur de l'utilisateur ne recevant que l'ID de session chiffré. Cette méthode est plus sécurisée, mais peut être complexe et utiliser plus de ressources serveur.

Bon à savoir : Bien que vous puissiez utiliser l'une ou l'autre méthode, ou les deux, nous recommandons d'utiliser une bibliothèque de gestion de sessions comme iron-session ou Jose.

Sessions sans état (Stateless)

Pour créer et gérer des sessions sans état, vous devez suivre quelques étapes :

  1. Générer une clé secrète, qui sera utilisée pour signer votre session, et la stocker comme variable d'environnement.
  2. Écrire une logique pour chiffrer/déchiffrer les données de session à l'aide d'une bibliothèque de gestion de sessions.
  3. Gérer les cookies avec l'API cookies de Next.js.

En plus de ce qui précède, envisagez d'ajouter des fonctionnalités pour mettre à jour (ou rafraîchir) la session lorsque l'utilisateur revient sur l'application, et supprimer la session lorsque l'utilisateur se déconnecte.

Bon à savoir : Vérifiez si votre bibliothèque d'authentification inclut la gestion des sessions.

1. Générer une clé secrète

Il existe plusieurs façons de générer une clé secrète pour signer votre session. Par exemple, vous pouvez utiliser la commande openssl dans votre terminal :

terminal
openssl rand -base64 32

Cette commande génère une chaîne aléatoire de 32 caractères que vous pouvez utiliser comme clé secrète et stocker dans votre fichier de variables d'environnement :

.env
SESSION_SECRET=votre_clé_secrète

Vous pouvez ensuite référencer cette clé dans votre logique de gestion des sessions :

app/lib/session.js
const secretKey = process.env.SESSION_SECRET

2. Chiffrer et déchiffrer les sessions

Ensuite, vous pouvez utiliser votre bibliothèque de gestion de sessions préférée pour chiffrer et déchiffrer les sessions. Continuons avec l'exemple précédent en utilisant Jose (compatible avec le Edge Runtime) et le package server-only de React pour garantir que votre logique de gestion des sessions ne s'exécute que côté serveur.

import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload: SessionPayload) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d')
    .sign(encodedKey)
}

export async function decrypt(session: string | undefined = '') {
  try {
    const { payload } = await jwtVerify(session, encodedKey, {
      algorithms: ['HS256'],
    })
    return payload
  } catch (error) {
    console.log('Échec de la vérification de la session')
  }
}

Conseils :

  • Le payload doit contenir les données utilisateur minimales et uniques qui seront utilisées dans les requêtes suivantes, comme l'ID de l'utilisateur, son rôle, etc. Il ne doit pas contenir d'informations personnelles comme un numéro de téléphone, une adresse email, des informations de carte de crédit, etc., ou des données sensibles comme des mots de passe.

3. Définir les cookies (options recommandées)

Pour stocker la session dans un cookie, utilisez l'API cookies de Next.js. Le cookie doit être défini côté serveur et inclure les options recommandées :

  • HttpOnly : Empêche JavaScript côté client d'accéder au cookie.
  • Secure : Utilise https pour envoyer le cookie.
  • SameSite : Spécifie si le cookie peut être envoyé avec des requêtes cross-site.
  • Max-Age ou Expires : Supprime le cookie après une certaine période.
  • Path : Définit le chemin URL pour le cookie.

Consultez MDN pour plus d'informations sur ces options.

import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  const session = await encrypt({ userId, expiresAt })
  const cookieStore = await cookies()

  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

Dans votre Server Action, vous pouvez invoquer la fonction createSession() et utiliser l'API redirect() pour rediriger l'utilisateur vers la page appropriée :

import { createSession } from '@/app/lib/session'

export async function signup(state: FormState, formData: FormData) {
  // Étapes précédentes :
  // 1. Valider les champs du formulaire
  // 2. Préparer les données pour l'insertion dans la base de données
  // 3. Insérer l'utilisateur dans la base de données ou appeler l'API d'une bibliothèque

  // Étapes actuelles :
  // 4. Créer une session utilisateur
  await createSession(user.id)
  // 5. Rediriger l'utilisateur
  redirect('/profile')
}

Conseils :

  • Les cookies doivent être définis côté serveur pour éviter toute manipulation côté client.
  • 🎥 Regardez : En savoir plus sur les sessions sans état et l'authentification avec Next.js → YouTube (11 minutes).

Mise à jour (ou rafraîchissement) des sessions

Vous pouvez également prolonger la durée d'expiration de la session. C'est utile pour maintenir l'utilisateur connecté après qu'il revient sur l'application. Par exemple :

import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
  const session = (await cookies()).get('session')?.value
  const payload = await decrypt(session)

  if (!session || !payload) {
    return null
  }

  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}

Conseil : Vérifiez si votre bibliothèque d'authentification prend en charge les jetons de rafraîchissement, qui peuvent être utilisés pour prolonger la session de l'utilisateur.

Suppression de la session

Pour supprimer la session, vous pouvez supprimer le cookie :

import 'server-only'
import { cookies } from 'next/headers'

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

Vous pouvez ensuite réutiliser la fonction deleteSession() dans votre application, par exemple lors de la déconnexion :

import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
  await deleteSession()
  redirect('/login')
}

Sessions de base de données

Pour créer et gérer des sessions de base de données, vous devrez suivre ces étapes :

  1. Créez une table dans votre base de données pour stocker les sessions et les données (ou vérifiez si votre bibliothèque d'authentification gère cela).
  2. Implémentez des fonctionnalités pour insérer, mettre à jour et supprimer des sessions.
  3. Chiffrez l'ID de session avant de le stocker dans le navigateur de l'utilisateur, et assurez-vous que la base de données et le cookie restent synchronisés (ceci est facultatif, mais recommandé pour les vérifications d'authentification optimistes dans le Middleware).

Par exemple :

import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'

export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  // 1. Créez une session dans la base de données
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Retournez l'ID de session
    .returning({ id: sessions.id })

  const sessionId = data[0].id

  // 2. Chiffrez l'ID de session
  const session = await encrypt({ sessionId, expiresAt })

  // 3. Stockez la session dans les cookies pour des vérifications d'authentification optimistes
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

Conseils :

  • Pour un accès plus rapide, vous pouvez envisager d'ajouter une mise en cache côté serveur pour la durée de vie de la session. Vous pouvez également conserver les données de session dans votre base de données principale et combiner les requêtes de données pour réduire le nombre de requêtes.
  • Vous pouvez choisir d'utiliser des sessions de base de données pour des cas d'utilisation plus avancés, comme suivre la dernière fois qu'un utilisateur s'est connecté, le nombre d'appareils actifs, ou donner aux utilisateurs la possibilité de se déconnecter de tous les appareils.

Après avoir implémenté la gestion des sessions, vous devrez ajouter une logique d'autorisation pour contrôler ce que les utilisateurs peuvent accéder et faire dans votre application. Passez à la section Autorisation pour en savoir plus.

Autorisation

Une fois qu'un utilisateur est authentifié et qu'une session est créée, vous pouvez implémenter l'autorisation pour contrôler ce que l'utilisateur peut accéder et faire dans votre application.

Il existe deux principaux types de vérifications d'autorisation :

  1. Optimiste : Vérifie si l'utilisateur est autorisé à accéder à une route ou à effectuer une action en utilisant les données de session stockées dans le cookie. Ces vérifications sont utiles pour des opérations rapides, comme afficher/masquer des éléments d'interface ou rediriger les utilisateurs en fonction des permissions ou rôles.
  2. Sécurisée : Vérifie si l'utilisateur est autorisé à accéder à une route ou à effectuer une action en utilisant les données de session stockées dans la base de données. Ces vérifications sont plus sécurisées et sont utilisées pour des opérations nécessitant un accès à des données sensibles ou des actions.

Pour les deux cas, nous recommandons :

Vérifications optimistes avec Middleware (Optionnel)

Il existe des cas où vous pouvez vouloir utiliser le Middleware et rediriger les utilisateurs en fonction des permissions :

  • Pour effectuer des vérifications optimistes. Comme le Middleware s'exécute sur chaque route, c'est un bon moyen de centraliser la logique de redirection et de pré-filtrer les utilisateurs non autorisés.
  • Pour protéger les routes statiques qui partagent des données entre utilisateurs (par exemple, du contenu derrière un paywall).

Cependant, comme le Middleware s'exécute sur chaque route, y compris les routes préchargées, il est important de ne lire la session que depuis le cookie (vérifications optimistes), et d'éviter les vérifications en base de données pour prévenir les problèmes de performance.

Par exemple :

import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. Spécifiez les routes protégées et publiques
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']

export default async function middleware(req: NextRequest) {
  // 2. Vérifiez si la route actuelle est protégée ou publique
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)

  // 3. Déchiffrez la session depuis le cookie
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  // 4. Redirigez vers /login si l'utilisateur n'est pas authentifié
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // 5. Redirigez vers /dashboard si l'utilisateur est authentifié
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }

  return NextResponse.next()
}

// Routes sur lesquelles le Middleware ne doit pas s'exécuter
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Bien que le Middleware puisse être utile pour des vérifications initiales, il ne devrait pas être votre seule ligne de défense pour protéger vos données. La majorité des vérifications de sécurité devraient être effectuées aussi près que possible de votre source de données, voir Couche d'Accès aux Données (DAL) pour plus d'informations.

Conseils :

  • Dans le Middleware, vous pouvez également lire les cookies en utilisant req.cookies.get('session').value.
  • Le Middleware utilise le Edge Runtime, vérifiez si votre bibliothèque d'authentification et votre bibliothèque de gestion de session sont compatibles.
  • Vous pouvez utiliser la propriété matcher dans le Middleware pour spécifier sur quelles routes le Middleware doit s'exécuter. Cependant, pour l'authentification, il est recommandé que le Middleware s'exécute sur toutes les routes.

Création d'une Couche d'Accès aux Données (DAL)

Nous recommandons de créer une DAL pour centraliser vos requêtes de données et votre logique d'autorisation.

La DAL devrait inclure une fonction qui vérifie la session de l'utilisateur lors de ses interactions avec votre application. Au minimum, la fonction devrait vérifier si la session est valide, puis rediriger ou retourner les informations utilisateur nécessaires pour effectuer d'autres requêtes.

Par exemple, créez un fichier séparé pour votre DAL qui inclut une fonction verifySession(). Utilisez ensuite l'API cache de React pour mémoïser la valeur de retour de la fonction pendant un rendu React :

import 'server-only'

import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect('/login')
  }

  return { isAuth: true, userId: session.userId }
})

Vous pouvez ensuite invoquer la fonction verifySession() dans vos requêtes de données, Actions Serveur, ou Gestionnaires de Route :

export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null

  try {
    const data = await db.query.users.findMany({
      where: eq(users.id, session.userId),
      // Retournez explicitement les colonnes dont vous avez besoin plutôt que l'objet utilisateur entier
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    const user = data[0]

    return user
  } catch (error) {
    console.log('Échec de la récupération de l\'utilisateur')
    return null
  }
})

Conseil :

  • Une DAL peut être utilisée pour protéger les données récupérées au moment de la requête. Cependant, pour les routes statiques qui partagent des données entre utilisateurs, les données seront récupérées au moment de la construction et non au moment de la requête. Utilisez le Middleware pour protéger les routes statiques.
  • Pour des vérifications sécurisées, vous pouvez vérifier si la session est valide en comparant l'ID de session avec votre base de données. Utilisez la fonction cache de React pour éviter des requêtes en double inutiles vers la base de données pendant un rendu.
  • Vous pouvez souhaiter consolider les requêtes de données liées dans une classe JavaScript qui exécute verifySession() avant toute méthode.

Utilisation des objets de transfert de données (DTO)

Lors de la récupération de données, il est recommandé de ne retourner que les données nécessaires qui seront utilisées dans votre application, et non des objets entiers. Par exemple, si vous récupérez des données utilisateur, vous pourriez ne retourner que l'ID et le nom de l'utilisateur, plutôt que l'objet utilisateur entier qui pourrait contenir des mots de passe, numéros de téléphone, etc.

Cependant, si vous n'avez pas le contrôle sur la structure des données retournées, ou si vous travaillez en équipe et souhaitez éviter que des objets entiers soient transmis au client, vous pouvez utiliser des stratégies comme spécifier quels champs peuvent être exposés en toute sécurité au client.

import 'server-only'
import { getUser } from '@/app/lib/dal'

function canSeeUsername(viewer: User) {
  return true
}

function canSeePhoneNumber(viewer: User, team: string) {
  return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
  const data = await db.query.users.findMany({
    where: eq(users.slug, slug),
    // Return specific columns here
  })
  const user = data[0]

  const currentUser = await getUser(user.id)

  // Or return only what's specific to the query here
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

En centralisant vos requêtes de données et votre logique d'autorisation dans une couche d'accès aux données (DAL) et en utilisant des DTO, vous pouvez garantir que toutes les requêtes de données sont sécurisées et cohérentes, ce qui facilite la maintenance, l'audit et le débogage à mesure que votre application évolue.

Bon à savoir :

  • Il existe plusieurs façons de définir un DTO, qu'il s'agisse d'utiliser toJSON(), des fonctions individuelles comme dans l'exemple ci-dessus, ou des classes JavaScript. Comme il s'agit de modèles JavaScript et non d'une fonctionnalité React ou Next.js, nous vous recommandons de faire des recherches pour trouver le modèle le plus adapté à votre application.
  • En savoir plus sur les bonnes pratiques de sécurité dans notre article sur la sécurité dans Next.js.

Composants serveur

Les vérifications d'authentification dans les composants serveur sont utiles pour un accès basé sur les rôles. Par exemple, pour afficher conditionnellement des composants en fonction du rôle de l'utilisateur :

import { verifySession } from '@/app/lib/dal'

export default function Dashboard() {
  const session = await verifySession()
  const userRole = session?.user?.role // En supposant que 'role' fait partie de l'objet session

  if (userRole === 'admin') {
    return <AdminDashboard />
  } else if (userRole === 'user') {
    return <UserDashboard />
  } else {
    redirect('/login')
  }
}

Dans cet exemple, nous utilisons la fonction verifySession() de notre DAL pour vérifier les rôles 'admin', 'user' et non autorisés. Ce modèle garantit que chaque utilisateur interagit uniquement avec les composants appropriés à son rôle.

Layouts et vérifications d'authentification

En raison du rendu partiel (Partial Rendering), soyez prudent lorsque vous effectuez des vérifications dans les layouts car ceux-ci ne sont pas re-rendus lors de la navigation, ce qui signifie que la session utilisateur ne sera pas vérifiée à chaque changement de route.

À la place, vous devriez effectuer les vérifications près de votre source de données ou du composant qui sera rendu conditionnellement.

Par exemple, considérez un layout partagé qui récupère les données utilisateur et affiche l'image de l'utilisateur dans une navigation. Au lieu d'effectuer la vérification d'authentification dans le layout, vous devriez récupérer les données utilisateur (getUser()) dans le layout et effectuer la vérification d'authentification dans votre DAL.

Cela garantit que partout où getUser() est appelé dans votre application, la vérification d'authentification est effectuée, et empêche les développeurs d'oublier de vérifier que l'utilisateur est autorisé à accéder aux données.

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();

  return (
    // ...
  )
}

Bon à savoir :

  • Un modèle courant dans les SPA est de return null dans un layout ou un composant de haut niveau si un utilisateur n'est pas autorisé. Ce modèle n'est pas recommandé car les applications Next.js ont plusieurs points d'entrée, ce qui n'empêchera pas les segments de route imbriqués et les actions serveur d'être accessibles.

Actions serveur

Traitez les actions serveur avec les mêmes considérations de sécurité que les points de terminaison d'API publics, et vérifiez si l'utilisateur est autorisé à effectuer une mutation.

Dans l'exemple ci-dessous, nous vérifions le rôle de l'utilisateur avant de permettre à l'action de se poursuivre :

'use server'
import { verifySession } from '@/app/lib/dal'

export async function serverAction(formData: FormData) {
  const session = await verifySession()
  const userRole = session?.user?.role

  // Retourner prématurément si l'utilisateur n'est pas autorisé à effectuer l'action
  if (userRole !== 'admin') {
    return null
  }

  // Poursuivre l'action pour les utilisateurs autorisés
}

Gestionnaires de route

Traitez les gestionnaires de route (Route Handlers) avec les mêmes considérations de sécurité que les points de terminaison d'API publics, et vérifiez si l'utilisateur est autorisé à accéder au gestionnaire de route.

Par exemple :

import { verifySession } from '@/app/lib/dal'

export async function GET() {
  // Authentification utilisateur et vérification du rôle
  const session = await verifySession()

  // Vérifier si l'utilisateur est authentifié
  if (!session) {
    // L'utilisateur n'est pas authentifié
    return new Response(null, { status: 401 })
  }

  // Vérifier si l'utilisateur a le rôle 'admin'
  if (session.user.role !== 'admin') {
    // L'utilisateur est authentifié mais n'a pas les bonnes permissions
    return new Response(null, { status: 403 })
  }

  // Continuer pour les utilisateurs autorisés
}

L'exemple ci-dessus démontre un gestionnaire de route avec une vérification de sécurité à deux niveaux. Il vérifie d'abord une session active, puis vérifie si l'utilisateur connecté est un 'admin'.

Fournisseurs de contexte

L'utilisation de fournisseurs de contexte pour l'authentification fonctionne grâce à l'imbrication (interleaving). Cependant, le context de React n'est pas pris en charge dans les composants serveur, ce qui les rend uniquement applicables aux composants client.

Cela fonctionne, mais tout composant serveur enfant sera d'abord rendu côté serveur et n'aura pas accès aux données de session du fournisseur de contexte :

import { ContextProvider } from 'auth-lib'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <ContextProvider>{children}</ContextProvider>
      </body>
    </html>
  )
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}

Si les données de session sont nécessaires dans les composants client (par exemple pour la récupération de données côté client), utilisez l'API taintUniqueValue de React pour empêcher que des données de session sensibles soient exposées au client.

Ressources

Maintenant que vous avez appris l'authentification dans Next.js, voici des bibliothèques compatibles avec Next.js et des ressources pour vous aider à implémenter une authentification et une gestion de session sécurisées :

Bibliothèques d'authentification

Bibliothèques de gestion de session

Pour aller plus loin

Pour continuer à apprendre sur l'authentification et la sécurité, consultez les ressources suivantes :