Pourquoi Authentification dans Next.js ?
L'authentification est un élément fondamental en développement web, car elle permet de protéger les ressources et les données sensibles des utilisateurs. En tant que développeur senior Next.js avec 10+ ans d'expérience, vous savez combien c'est crucial de mettre en place une authentification robuste pour votre application. Dans un contexte réel, un développeur a besoin d'une solution d'authentification pour plusieurs raisons :
- Sécurité des données : Protéger les informations personnelles et sensibles des utilisateurs.
- Contrôle d'accès : Permettre aux utilisateurs de se connecter à leur compte et d'effectuer des actions spécifiques.
- Gestion des permissions : Définir les droits d'accès pour chaque utilisateur ou groupe.
Un cas d'utilisation concret serait un gestionnaire de tâches où les utilisateurs doivent être authentifiés pour voir, ajouter, modifier ou supprimer leurs propres tâches. Sans une authentification fiable, ces informations seraient vulnérables et potentiellement accessibles par n'importe qui.
Prerequis
Connaissances nécessaires :
- JavaScript ES6+
- Concepts de React
- Compréhension de Next.js (composants, routes, API routes)
- Familiarité avec les bases de données (ex: MongoDB, PostgreSQL)
Outils à installer :
- Node.js (v14+)
- npm (Node Package Manager)
- MongoDB (ou une autre base de données compatible)
Concepts fondamentaux
1. Authentification vs Autorisation
Authentification : Vérifier l'identité d'un utilisateur en vérifiant ses identifiants (login/password).
Autorisation : Définir les permissions et droits d'accès une fois que l'utilisateur est authentifié.
// Exemple de structure pour authentification et autorisation
const isAuthenticated = async (req, res) => {
// Vérifie si le token JWT est présent dans les cookies ou headers
const token = req.cookies.token;
if (!token) return false;
try {
const decoded = jwt.verify(token, 'secret-key');
// Vérifie si l'utilisateur a les permissions nécessaires
const userRole = getUserRole(decoded.userId);
return userRole === 'admin';
} catch (error) {
return false;
}
};
2. JWT (JSON Web Token)
JWT est un format compact et sécurisé pour transmettre des informations entre parties en tant qu'assertions JSON. Il est largement utilisé dans l'authentification.
// Exemple de génération et de vérification d'un JWT
const generateToken = (userId) => {
return jwt.sign({ userId }, 'secret-key', { expiresIn: '1h' });
};
const verifyToken = async (token) => {
try {
const decoded = await jwt.verify(token, 'secret-key');
return decoded;
} catch (error) {
throw new Error('Invalid token');
}
};
3. Middleware d'authentification
Un middleware est une fonction qui s'exécute entre la requête et la réponse du serveur. Elle peut être utilisée pour vérifier l'authentification avant de permettre l'accès à certaines routes.
// Exemple de middleware d'authentification
const withAuth = (WrappedComponent) => {
return (props) => {
const isAuthenticated = props.isAuthenticated;
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
return <WrappedComponent {...props} />;
};
};
export default withAuth;
4. Utilisation des hooks React
Les hooks de React comme useState et useEffect peuvent être utilisés pour gérer l'état d'authentification.
// Exemple d'utilisation de useState et useEffect pour gérer l'état d'authentification
const useAuth = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const token = localStorage.getItem('token');
if (token) {
verifyToken(token).then((decoded) => {
setIsAuthenticated(true);
});
}
}, []);
return { isAuthenticated };
};
export default useAuth;
Mise en pratique : Projet fil rouge
Pour illustrer la mise en place de l'authentification dans Next.js, nous allons créer un simple gestionnaire de tâches. Ce projet comprendra les fonctionnalités suivantes :
- Connexion et déconnexion
- Création et affichage des tâches
- Édition et suppression des tâches
Étape 1 : Initialisation du projet
npx create-next-app@latest task-manager
cd task-manager
Étape 2 : Installation des dépendances
npm install next-auth jsonwebtoken bcryptjs mongoose
Étape 3 : Configuration de Next-Auth
Créez un fichier pages/api/auth/[...nextauth].js :
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import bcrypt from 'bcryptjs';
import User from '../../../models/User';
export default NextAuth({
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials) {
const user = await User.findOne({ email: credentials.email });
if (user && bcrypt.compareSync(credentials.password, user.password)) {
return user;
} else {
return null;
}
}
})
],
callbacks: {
jwt: async (token, user) => {
if (user) {
token.id = user._id;
}
return Promise.resolve(token);
},
session: async (session, token) => {
session.userId = token.id;
return Promise.resolve(session);
}
}
});
Étape 4 : Création du modèle Mongoose
Créez un fichier models/User.js :
import mongoose from 'mongoose';
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
export default mongoose.model('User', UserSchema);
Étape 5 : Création des composants
Créez un fichier components/AuthForm.js :
import React, { useState } from 'react';
import { useRouter } from 'next/router';
const AuthForm = ({ isLogin }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (isLogin) {
// Connexion
const response = await fetch('/api/auth/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const result = await response.json();
if (result.ok) {
router.push('/');
} else {
alert(result.message);
}
} else {
// Inscription
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const result = await response.json();
if (result.ok) {
router.push('/login');
} else {
alert(result.message);
}
}
} catch (error) {
console.error(error);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">{isLogin ? 'Connectez-vous' : 'Inscrivez-vous'}</button>
</form>
);
};
export default AuthForm;
Créez un fichier pages/login.js :
import AuthForm from '../components/AuthForm';
const LoginPage = () => {
return <AuthForm isLogin />;
};
export default LoginPage;
Créez un fichier pages/signup.js :
import AuthForm from '../components/AuthForm';
const SignupPage = () => {
return <AuthForm isLogin={false} />;
};
export default SignupPage;
Étape 6 : Création des pages protégées
Créez un fichier pages/tasks.js et ajoutez le middleware d'authentification :
import { useEffect, useState } from 'react';
import useAuth from '../components/useAuth';
const TasksPage = () => {
const { isAuthenticated } = useAuth();
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (!isAuthenticated) return;
fetch('/api/tasks')
.then((response) => response.json())
.then((data) => setTasks(data));
}, [isAuthenticated]);
return (
<div>
{isAuthenticated ? (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
) : (
<p>Veuillez vous connecter pour voir vos tâches.</p>
)}
</div>
);
};
export default TasksPage;
Étape 7 : Création des API routes
Créez un fichier pages/api/tasks.js :
import User from '../../models/User';
import Task from '../../models/Task';
const handler = async (req, res) => {
const { method } = req;
if (method === 'GET') {
// Récupérer les tâches de l'utilisateur connecté
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, 'secret-key');
const tasks = await Task.find({ userId: decoded.id });
res.status(200).json(tasks);
} else if (method === 'POST') {
// Créer une nouvelle tâche
const { title } = req.body;
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, 'secret-key');
const task = new Task({ title, userId: decoded.id });
await task.save();
res.status(201).json(task);
}
};
export default handler;
Créez un fichier pages/api/tasks/[id].js :
import User from '../../models/User';
import Task from '../../models/Task';
const handler = async (req, res) => {
const { method } = req;
const { id } = req.query;
if (method === 'GET') {
// Récupérer une tâche spécifique
const task = await Task.findById(id);
res.status(200).json(task);
} else if (method === 'PUT') {
// Mettre à jour une tâche
const { title, completed } = req.body;
const task = await Task.findByIdAndUpdate(id, { title, completed }, { new: true });
res.status(200).json(task);
} else if (method === 'DELETE') {
// Supprimer une tâche
await Task.findByIdAndDelete(id);
res.status(204).end();
}
};
export default handler;
Erreurs fréquentes et debugging
1. Erreur : Token invalide
Code incorrect :
const decoded = jwt.verify(token, 'wrong-secret-key');
Code correct :
try {
const decoded = jwt.verify(token, 'secret-key');
} catch (error) {
throw new Error('Invalid token');
}
2. Erreur : Utilisateur non trouvé
Code incorrect :
const user = await User.findOne({ email });
if (!user) return null;
Code correct :
const user = await User.findOne({ email });
if (!user) throw new Error('User not found');
3. Erreur : Route non autorisée
Code incorrect :
if (req.method !== 'GET') {
return res.status(405).end();
}
Code correct :
if (!isAuthenticated) return res.status(401).json({ message: 'Unauthorized' });
if (req.method === 'GET') {
// Logique pour la méthode GET
} else if (req.method === 'POST') {
// Logique pour la méthode POST
}
Pour aller plus loin
1. Utilisation de OAuth pour une connexion sociale
Incorporer des options de connexion via Google, Facebook ou Twitter.
2. Gestion des rôles et permissions
Définir différents niveaux d'accès et limiter certaines actions en fonction du rôle de l'utilisateur.
3. Sécurité supplémentaire
Utiliser HTTPS, ajouter des limites d'IP, configurer CORS appropriéement.
Défi pratique
Développer un système de notifications pour les utilisateurs lorsqu'ils créent ou modifient une tâche. Utilisez Next-Auth pour la gestion des sessions et MongoDB pour stocker les notifications.