Modèles d'implémentation de l'authentification et de l'autorisation
Construisez des systèmes d'authentification et d'autorisation sécurisés et scalables en utilisant des modèles standard du secteur et les meilleures pratiques modernes.
Quand utiliser cette compétence
- Implémenter des systèmes d'authentification utilisateur
- Sécuriser les API REST ou GraphQL
- Ajouter OAuth2/connexion sociale
- Implémenter le contrôle d'accès basé sur les rôles (RBAC)
- Concevoir la gestion des sessions
- Migrer les systèmes d'authentification
- Déboguer les problèmes d'authentification
- Implémenter le SSO ou le multi-tenant
Concepts fondamentaux
1. Authentification vs Autorisation
Authentification (AuthN) : Qui êtes-vous ?
- Vérification de l'identité (nom d'utilisateur/mot de passe, OAuth, biométrie)
- Émission de credentials (sessions, tokens)
- Gestion de la connexion/déconnexion
Autorisation (AuthZ) : Que pouvez-vous faire ?
- Vérification des permissions
- Contrôle d'accès basé sur les rôles (RBAC)
- Validation de la propriété des ressources
- Application des politiques
2. Stratégies d'authentification
Basée sur les sessions :
- Le serveur stocke l'état de la session
- ID de session dans un cookie
- Traditionnelle, simple, avec état
Basée sur les tokens (JWT) :
- Sans état, autonome
- Scalable horizontalement
- Peut stocker des claims
OAuth2/OpenID Connect :
- Déléguer l'authentification
- Connexion sociale (Google, GitHub)
- SSO d'entreprise
Authentification JWT
Pattern 1 : Implémentation JWT
// Structure JWT : header.payload.signature
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
interface JWTPayload {
userId: string;
email: string;
role: string;
iat: number;
exp: number;
}
// Générer JWT
function generateTokens(userId: string, email: string, role: string) {
const accessToken = jwt.sign(
{ userId, email, role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" }, // Courte durée de vie
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: "7d" }, // Longue durée de vie
);
return { accessToken, refreshToken };
}
// Vérifier JWT
function verifyToken(token: string): JWTPayload {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error("Token expired");
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error("Invalid token");
}
throw error;
}
}
// Middleware
function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ error: "No token provided" });
}
const token = authHeader.substring(7);
try {
const payload = verifyToken(token);
req.user = payload; // Attacher l'utilisateur à la requête
next();
} catch (error) {
return res.status(401).json({ error: "Invalid token" });
}
}
// Utilisation
app.get("/api/profile", authenticate, (req, res) => {
res.json({ user: req.user });
});
Pattern 2 : Flux de refresh token
interface StoredRefreshToken {
token: string;
userId: string;
expiresAt: Date;
createdAt: Date;
}
class RefreshTokenService {
// Stocker le refresh token en base de données
async storeRefreshToken(userId: string, refreshToken: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await db.refreshTokens.create({
token: await hash(refreshToken), // Hacher avant de stocker
userId,
expiresAt,
});
}
// Rafraîchir le token d'accès
async refreshAccessToken(refreshToken: string) {
// Vérifier le refresh token
let payload;
try {
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as {
userId: string;
};
} catch {
throw new Error("Invalid refresh token");
}
// Vérifier si le token existe en base de données
const storedToken = await db.refreshTokens.findOne({
where: {
token: await hash(refreshToken),
userId: payload.userId,
expiresAt: { $gt: new Date() },
},
});
if (!storedToken) {
throw new Error("Refresh token not found or expired");
}
// Récupérer l'utilisateur
const user = await db.users.findById(payload.userId);
if (!user) {
throw new Error("User not found");
}
// Générer un nouveau token d'accès
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" },
);
return { accessToken };
}
// Révoquer le refresh token (déconnexion)
async revokeRefreshToken(refreshToken: string) {
await db.refreshTokens.deleteOne({
token: await hash(refreshToken),
});
}
// Révoquer tous les tokens de l'utilisateur (déconnexion de tous les appareils)
async revokeAllUserTokens(userId: string) {
await db.refreshTokens.deleteMany({ userId });
}
}
// Points de terminaison API
app.post("/api/auth/refresh", async (req, res) => {
const { refreshToken } = req.body;
try {
const { accessToken } =
await refreshTokenService.refreshAccessToken(refreshToken);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: "Invalid refresh token" });
}
});
app.post("/api/auth/logout", authenticate, async (req, res) => {
const { refreshToken } = req.body;
await refreshTokenService.revokeRefreshToken(refreshToken);
res.json({ message: "Logged out successfully" });
});
Authentification basée sur les sessions
Pattern 1 : Express Session
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
// Configurer Redis pour le stockage des sessions
const redisClient = createClient({
url: process.env.REDIS_URL,
});
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === "production", // HTTPS uniquement
httpOnly: true, // Pas d'accès JavaScript
maxAge: 24 * 60 * 60 * 1000, // 24 heures
sameSite: "strict", // Protection CSRF
},
}),
);
// Connexion
app.post("/api/auth/login", async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({ email });
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Stocker l'utilisateur dans la session
req.session.userId = user.id;
req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email, role: user.role } });
});
// Middleware de session
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.session.userId) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
}
// Route protégée
app.get("/api/profile", requireAuth, async (req, res) => {
const user = await db.users.findById(req.session.userId);
res.json({ user });
});
// Déconnexion
app.post("/api/auth/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Logout failed" });
}
res.clearCookie("connect.sid");
res.json({ message: "Logged out successfully" });
});
});
OAuth2 / Connexion sociale
Pattern 1 : OAuth2 avec Passport.js
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as GitHubStrategy } from "passport-github2";
// OAuth Google
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "/api/auth/google/callback",
},
async (accessToken, refreshToken, profile, done) => {
try {
// Rechercher ou créer un utilisateur
let user = await db.users.findOne({
googleId: profile.id,
});
if (!user) {
user = await db.users.create({
googleId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName,
avatar: profile.photos?.[0]?.value,
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
},
),
);
// Routes
app.get(
"/api/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"],
}),
);
app.get(
"/api/auth/google/callback",
passport.authenticate("google", { session: false }),
(req, res) => {
// Générer JWT
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
// Rediriger vers le frontend avec le token
res.redirect(
`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`,
);
},
);
Modèles d'autorisation
Pattern 1 : Contrôle d'accès basé sur les rôles (RBAC)
enum Role {
USER = "user",
MODERATOR = "moderator",
ADMIN = "admin",
}
const roleHierarchy: Record<Role, Role[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
[Role.USER]: [Role.USER],
};
function hasRole(userRole: Role, requiredRole: Role): boolean {
return roleHierarchy[userRole].includes(requiredRole);
}
// Middleware
function requireRole(...roles: Role[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
if (!roles.some((role) => hasRole(req.user.role, role))) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
// Utilisation
app.delete(
"/api/users/:id",
authenticate,
requireRole(Role.ADMIN),
async (req, res) => {
// Seuls les admins peuvent supprimer des utilisateurs
await db.users.delete(req.params.id);
res.json({ message: "User deleted" });
},
);
Pattern 2 : Contrôle d'accès basé sur les permissions
enum Permission {
READ_USERS = "read:users",
WRITE_USERS = "write:users",
DELETE_USERS = "delete:users",
READ_POSTS = "read:posts",
WRITE_POSTS = "write:posts",
}
const rolePermissions: Record<Role, Permission[]> = {
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
[Role.MODERATOR]: [
Permission.READ_POSTS,
Permission.WRITE_POSTS,
Permission.READ_USERS,
],
[Role.ADMIN]: Object.values(Permission),
};
function hasPermission(userRole: Role, permission: Permission): boolean {
return rolePermissions[userRole]?.includes(permission) ?? false;
}
function requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const hasAllPermissions = permissions.every((permission) =>
hasPermission(req.user.role, permission),
);
if (!hasAllPermissions) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
}
// Utilisation
app.get(
"/api/users",
authenticate,
requirePermission(Permission.READ_USERS),
async (req, res) => {
const users = await db.users.findAll();
res.json({ users });
},
);
Pattern 3 : Propriété des ressources
// Vérifier si l'utilisateur possède la ressource
async function requireOwnership(
resourceType: "post" | "comment",
resourceIdParam: string = "id",
) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const resourceId = req.params[resourceIdParam];
// Les admins peuvent accéder à n'importe quoi
if (req.user.role === Role.ADMIN) {
return next();
}
// Vérifier la propriété
let resource;
if (resourceType === "post") {
resource = await db.posts.findById(resourceId);
} else if (resourceType === "comment") {
resource = await db.comments.findById(resourceId);
}
if (!resource) {
return res.status(404).json({ error: "Resource not found" });
}
if (resource.userId !== req.user.userId) {
return res.status(403).json({ error: "Not authorized" });
}
next();
};
}
// Utilisation
app.put(
"/api/posts/:id",
authenticate,
requireOwnership("post"),
async (req, res) => {
// L'utilisateur ne peut mettre à jour que ses propres messages
const post = await db.posts.update(req.params.id, req.body);
res.json({ post });
},
);
Meilleures pratiques de sécurité
Pattern 1 : Sécurité des mots de passe
import bcrypt from "bcrypt";
import { z } from "zod";
// Schéma de validation des mots de passe
const passwordSchema = z
.string()
.min(12, "Password must be at least 12 characters")
.regex(/[A-Z]/, "Password must contain uppercase letter")
.regex(/[a-z]/, "Password must contain lowercase letter")
.regex(/[0-9]/, "Password must contain number")
.regex(/[^A-Za-z0-9]/, "Password must contain special character");
// Hacher le mot de passe
async function hashPassword(password: string): Promise<string> {
const saltRounds = 12; // 2^12 itérations
return bcrypt.hash(password, saltRounds);
}
// Vérifier le mot de passe
async function verifyPassword(
password: string,
hash: string,
): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Inscription avec validation du mot de passe
app.post("/api/auth/register", async (req, res) => {
try {
const { email, password } = req.body;
// Valider le mot de passe
passwordSchema.parse(password);
// Vérifier si l'utilisateur existe
const existingUser = await db.users.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: "Email already registered" });
}
// Hacher le mot de passe
const passwordHash = await hashPassword(password);
// Créer l'utilisateur
const user = await db.users.create({
email,
passwordHash,
});
// Générer les tokens
const tokens = generateTokens(user.id, user.email, user.role);
res.status(201).json({
user: { id: user.id, email: user.email },
...tokens,
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: error.errors[0].message });
}
res.status(500).json({ error: "Registration failed" });
}
});
Pattern 2 : Limitation de débit
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
// Limiteur de débit pour la connexion
const loginLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 tentatives
message: "Too many login attempts, please try again later",
standardHeaders: true,
legacyHeaders: false,
});
// Limiteur de débit pour l'API
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requêtes par minute
standardHeaders: true,
});
// Appliquer aux routes
app.post("/api/auth/login", loginLimiter, async (req, res) => {
// Logique de connexion
});
app.use("/api/", apiLimiter);
Meilleures pratiques
- Ne jamais stocker les mots de passe en clair : Toujours les hacher avec bcrypt/argon2
- Utiliser HTTPS : Chiffrer les données en transit
- Tokens d'accès à courte durée : 15-30 minutes maximum
- Cookies sécurisés : Drapeaux httpOnly, secure, sameSite
- Valider toutes les entrées : Format d'email, force du mot de passe
- Limiter le débit des points de terminaison d'authentification : Prévenir les attaques par force brute
- Implémenter la protection CSRF : Pour l'authentification basée sur les sessions
- Faire pivoter les secrets régulièrement : Secrets JWT, secrets de session
- Enregistrer les événements de sécurité : Tentatives de connexion, authentification échouée
- Utiliser l'authentification multifacteur si possible : Couche de sécurité supplémentaire
Pièges courants
- Mots de passe faibles : Appliquer des politiques de mots de passe forts
- JWT dans localStorage : Vulnérable aux attaques XSS, utiliser des cookies httpOnly
- Pas d'expiration des tokens : Les tokens doivent expirer
- Vérifications d'authentification uniquement côté client : Toujours valider côté serveur
- Réinitialisation de mot de passe non sécurisée : Utiliser des tokens sécurisés avec expiration
- Pas de limitation de débit : Vulnérable aux attaques par force brute
- Faire confiance aux données du client : Toujours valider sur le serveur