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

Voici les étapes pour implémenter un formulaire d'inscription et/ou de connexion :

  1. L'utilisateur soumet ses identifiants via un formulaire.
  2. Le formulaire envoie une requête traitée par une route API.
  3. Après vérification réussie, le processus est terminé, indiquant que l'utilisateur est authentifié.
  4. Si la vérification échoue, un message d'erreur est affiché.

Prenons l'exemple d'un formulaire de connexion où les utilisateurs peuvent saisir leurs identifiants :

import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      router.push('/profile')
    } else {
      // Gérer les erreurs
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Mot de passe" required />
      <button type="submit">Se connecter</button>
    </form>
  )
}

Le formulaire ci-dessus a deux champs pour capturer l'email et le mot de passe de l'utilisateur. Lors de la soumission, il déclenche une fonction qui envoie une requête POST à une route API (/api/auth/login).

Vous pouvez ensuite appeler l'API de votre fournisseur d'authentification dans la route API pour gérer l'authentification :

import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: 'Identifiants invalides.' })
    } else {
      res.status(500).json({ error: 'Une erreur est survenue.' })
    }
  }
}

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)

Définition et suppression des cookies

Vous pouvez utiliser les Routes API pour définir la session comme un cookie côté serveur :

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // Une semaine
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: 'Cookie défini avec succès !' })
}

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).

Création d'une session côté serveur :

import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: 'Erreur interne du serveur' })
  }
}

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)

Protection des routes API

Les routes API dans Next.js sont essentielles pour gérer la logique côté serveur et la gestion des données. Il est crucial de sécuriser ces routes pour garantir que seuls les utilisateurs autorisés peuvent accéder à des fonctionnalités spécifiques. Cela implique généralement de vérifier le statut d'authentification de l'utilisateur et ses permissions basées sur les rôles.

Voici un exemple de sécurisation d'une route API :

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)

  // Vérifier si l'utilisateur est authentifié
  if (!session) {
    res.status(401).json({
      error: 'L\'utilisateur n\'est pas authentifié',
    })
    return
  }

  // Vérifier si l'utilisateur a le rôle 'admin'
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Accès non autorisé : L\'utilisateur n\'a pas les privilèges admin.',
    })
    return
  }

  // Poursuivre la route pour les utilisateurs autorisés
  // ... implémentation de la route API
}

Cet exemple démontre une route API avec une vérification de sécurité à deux niveaux pour l'authentification et l'autorisation. Il vérifie d'abord une session active, puis vérifie si l'utilisateur connecté est un 'admin'. Cette approche garantit un accès sécurisé, limité aux utilisateurs authentifiés et autorisés, maintenant une sécurité robuste pour le traitement des requêtes.

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 :