Skip to main content

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 authenticateToken est ajouté en tant que deuxième paramètre pour protéger les routes. Il sera exécuté avant le handler de la route.