7. Authentification
Mettre en place un système d'authentification avec JWT dans Node.js
L'authentification permet de vérifier l'identité d'un utilisateur. Dans ce cours, nous allons mettre en place un système de login qui génère un JWT (JSON Web Token), puis nous allons protéger certaines routes de notre API pour qu'elles ne soient accessibles qu'aux utilisateurs authentifiés.
Concepts de base
JWT
Les tokens d'authentification sont des jetons qui sont obtenus lors de la connexion d'un utilisateur. Ce dernier reçoit
une clé cryptée qui contient des informations permettant de l'identifier. Ces jetons sont souvents stockés dans le
LocalStorage ou les Cookies sur les navigateurs.
À chaque fois que l'utilisateur tentera de se rendre sur des ressources sécurisées le serveur vérifiera la validé du
token. Si le token est correct et correspond bien à l'utilisateur alors il autorisera l'accès à la ressource sinon il le
rejettera (erreur 401 Unauthorized).
Les tokens sont donc très pratique car ils évitent à un utilisateur de devoir se reconnecter systmétiquement à chaque requête au serveur. D'autant qu'on peut leur donner une durée de vie ce qui permettra d'éviter la reconnexion pendant ce laps de temps.
Pour utiliser les tokens avec nodeJS nous utilisons ce que l'on appelle les jsonwebtoken ou JWT. Il s'agit d'un format standard défini ici. Sa structure se décompose en 3 parties:
HEADER.PAYLOAD.SIGNATURE
HEADER: en-tête qui définit le type de token ainsi que son algorithme d'encryptage de signature. C'est un objet JSON.PAYLOAD: qui possède les data que l'on souhaite stocker dans le JWT, comme l'id utilisateur, son rôle, (...). C'est un objet JSON.SIGNATURE: Une signature numérique qui permet le chiffrement et le déchiffrement de notre JWT. On l'obtient en chiffrant le HEADER et le PAYLOAD avec l'encodage base64url. Ensuite, on les concatène en les séparant par un point. On obtient la signature de ce résultat avec l'algorithme choisi. Cette signature est ajoutée au résultat de la même manière (encodée et séparée par un point). Généralement on rajoute à cela une clé de chiffrement définie par nos soins. Le chiffrement est capital car il permet de vérifier l'intégrité du token.
RÈGLE D'OR** : Ne JAMAIS stocker les mots de passe en clair ! On utilise bcrypt pour hasher (chiffrer de manière irréversible) les mots de passe avant de les stocker en base de données.
Mise en place
Installer les dépendances
npm install bcrypt jsonwebtoken dotenv
npm install -D @types/bcrypt @types/jsonwebtoken
Configuration de dotenv : Le package dotenv permet de charger les variables d'environnement depuis un fichier
.env. C'est essentiel pour stocker de manière sécurisée des informations sensibles comme la clé secrète JWT ou l'URL
de la base de données. Pour l'activer, ajoutez simplement cette ligne au tout début de votre fichier principal (
index.ts ou app.ts) :
import 'dotenv/config'
Cette import doit être la première ligne avant toute autre import pour garantir que les variables d'environnement sont disponibles partout dans votre application.
Schéma Prisma
Ajoutons un champ password à notre modèle User :
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
}
Créez la migration :
npx prisma migrate dev --name add_password
Seed
Modifions notre fichier prisma/seed.ts pour ajouter des mots de passe hashés :
import {PrismaBetterSqlite3} from '@prisma/adapter-better-sqlite3'
import bcrypt from 'bcrypt'
import {PrismaClient} from "@/generated/prisma/client";
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || 'file:./dev.db',
})
const prisma = new PrismaClient({adapter})
async function main() {
await prisma.user.deleteMany()
await prisma.$executeRaw`DELETE
FROM sqlite_sequence
WHERE name = 'User'`
// Tous les utilisateurs auront le mot de passe "password123"
const hashedPassword = await bcrypt.hash('password123', 10)
await prisma.user.createMany({
data: [
{
name: 'Alice',
email: 'alice@example.com',
password: hashedPassword,
},
{
name: 'Bob',
email: 'bob@example.com',
password: hashedPassword,
},
{
name: 'John Doe',
email: 'john@example.com',
password: hashedPassword,
},
],
})
console.log('Base de données peuplée avec succès !')
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
Exécutez le seed :
npx prisma db seed
Variables d'environnement
Créez un fichier .env à la racine du projet :
DATABASE_URL="file:./dev.db"
JWT_SECRET=votre_cle_secrete_tres_longue_et_complexe_ici
Important : Ne commitez JAMAIS le fichier
.env! Ajoutez-le au.gitignore.
Mettre à jour l'API
Endpoint Login
Fichier auth.route.ts :
import {Request, Response, Router} from 'express'
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import prisma from "@/client";
export const authRouter = Router()
// POST /auth/login
// Accessible via POST /auth/login
authRouter.post('/login', async (req: Request, res: Response) => {
const {email, password} = req.body
try {
// 1. Vérifier que l'utilisateur existe
const user = await prisma.user.findUnique({
where: {email},
})
if (!user) {
return res.status(401).json({error: 'Email ou mot de passe incorrect'})
}
// 2. Vérifier le mot de passe
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
return res.status(401).json({error: 'Email ou mot de passe incorrect'})
}
// 3. Générer le JWT
const token = jwt.sign(
{
userId: user.id,
email: user.email,
},
process.env.JWT_SECRET as string,
{expiresIn: '1h'}, // Le token expire dans 1 heure
)
// 4. Retourner le token
return res.status(200).json({
message: 'Connexion réussie',
token,
user: {
id: user.id,
name: user.name,
email: user.email,
},
})
} catch (error) {
console.error('Erreur lors de la connexion:', error)
return res.status(500).json({error: 'Erreur serveur'})
}
})
Remarque : N'oubliez pas d'importer et d'utiliser ce router dans votre fichier principal avec
app.use('/auth', authRouter).
Middleware d'authentification
Le middleware vérifie que le JWT est valide avant d'autoriser l'accès à une route.
import {NextFunction, Request, Response} from 'express'
import jwt from 'jsonwebtoken'
// Étendre le type Request pour ajouter userId
declare global {
namespace Express {
interface Request {
userId?: number
}
}
}
export const authenticateToken = (
req: Request,
res: Response,
next: NextFunction,
) => {
// 1. Récupérer le token depuis l'en-tête Authorization
const authHeader = req.headers.authorization
const token = authHeader && authHeader.split(' ')[1] // Format: "Bearer TOKEN"
if (!token) {
return res.status(401).json({error: 'Token manquant'})
}
try {
// 2. Vérifier et décoder le token
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as {
userId: number
email: string
}
// 3. Ajouter userId à la requête pour l'utiliser dans les routes
req.userId = decoded.userId
// 4. Passer au prochain middleware ou à la route
next()
} catch (error) {
return res.status(403).json({error: 'Token invalide ou expiré'})
}
}
Protéger les routes
Maintenant, nous pouvons protéger certaines routes pour qu'elles ne soient accessibles qu'aux utilisateurs authentifiés.
Fichier user.route.ts :
import {Request, Response, Router} from 'express'
import bcrypt from 'bcrypt'
import prisma from "@/client";
import {authenticateToken} from "@/auth/auth.middleware";
export const userRouter = Router()
// Route protégée : seuls les utilisateurs authentifiés peuvent créer un utilisateur
// Accessible via POST /users
userRouter.post('/', authenticateToken, async (req: Request, res: Response) => {
const {name, email, password} = req.body
try {
const hashedPassword = await bcrypt.hash(password, 10)
const user = await prisma.user.create({
data: {name, email, password: hashedPassword},
select: {
id: true,
name: true,
email: true,
},
})
res.status(201).json({
message: 'Utilisateur créé',
user,
})
} catch (error: any) {
res.status(400).json({error: error.message})
}
})
Important : Le middleware
authenticateTokenest ajouté en tant que deuxième paramètre pour protéger les routes. Il sera exécuté avant le handler de la route.