Sécurité WordPress : Protéger son site contre les attaques courantes
Introduction La sécurité WordPress n'est pas optionnelle. Avec plus de 43% des sites web utilisant…
L’authentification et l’autorisation sont les piliers de la sécurité des APIs modernes. En 2025, avec plus de 34% des incidents de sécurité liés à des APIs mal sécurisées, maîtriser les différentes stratégies d’authentification n’est plus optionnel. Ce guide complet couvre les trois approches principales : JWT (JSON Web Tokens), OAuth 2.0, et API Keys, avec des implémentations complètes et des considérations de sécurité.
| Critère | JWT | OAuth 2.0 | API Keys |
|---|---|---|---|
| Cas d’usage | API stateless, microservices | Délégation d’accès, SSO | Services to services |
| Complexité | Moyenne | Élevée | Faible |
| Sécurité | Haute (si bien implémenté) | Très haute | Moyenne |
| Scalabilité | Excellente (stateless) | Bonne | Excellente |
| Expiration | Oui (configurable) | Oui (refresh tokens) | Non (sauf implémentation custom) |
| Révocation | Difficile | Facile | Facile |
| Performance | Excellente | Moyenne | Excellente |
JWT:
OAuth 2.0:
API Keys:
Un JWT est composé de trois parties séparées par des points:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c
Décodé:
// Header
{
"alg": "HS256",
"typ": "JWT"
}
// Payload
{
"sub": "1234567890",
"name": "John Doe",
"email": "john@exemple.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
// Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
// config/jwt.config.js
module.exports = {
secret: process.env.JWTSECRET || 'votre-secret-super-securise-changez-moi',
accessTokenExpiry: '15m', // Access token: 15 minutes
refreshTokenExpiry: '7d', // Refresh token: 7 jours
algorithm: 'HS256',
issuer: 'api.exemple.com',
audience: 'exemple.com'
};
// utils/jwt.util.js
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const config = require('../config/jwt.config');
class JWTUtil {
/
Générer un access token
/
static generateAccessToken(user) {
const payload = {
sub: user.id,
email: user.email,
role: user.role,
type: 'access'
};
return jwt.sign(payload, config.secret, {
expiresIn: config.accessTokenExpiry,
algorithm: config.algorithm,
issuer: config.issuer,
audience: config.audience
});
}
/
Générer un refresh token
/
static generateRefreshToken(user) {
const payload = {
sub: user.id,
type: 'refresh',
jti: crypto.randomBytes(32).toString('hex') // JWT ID unique
};
return jwt.sign(payload, config.secret, {
expiresIn: config.refreshTokenExpiry,
algorithm: config.algorithm,
issuer: config.issuer,
audience: config.audience
});
}
/
Vérifier un token
/
static verifyToken(token) {
try {
return jwt.verify(token, config.secret, {
algorithms: [config.algorithm],
issuer: config.issuer,
audience: config.audience
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token expired');
}
if (error.name === 'JsonWebTokenError') {
throw new Error('Invalid token');
}
throw error;
}
}
/
Décoder un token sans vérification (pour debug)
/
static decodeToken(token) {
return jwt.decode(token, { complete: true });
}
}
module.exports = JWTUtil;
// middleware/auth.middleware.js
const JWTUtil = require('../utils/jwt.util');
const User = require('../models/User');
const authenticate = async (req, res, next) => {
try {
// Extraire le token du header Authorization
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Unauthorized',
message: 'No token provided'
});
}
const token = authHeader.substring(7); // Retirer "Bearer "
// Vérifier le token
const decoded = JWTUtil.verifyToken(token);
// Vérifier que c'est un access token
if (decoded.type !== 'access') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token type'
});
}
// Charger l'utilisateur
const user = await User.findByPk(decoded.sub);
if (!user) {
return res.status(401).json({
error: 'Unauthorized',
message: 'User not found'
});
}
// Vérifier que l'utilisateur est actif
if (!user.isactive) {
return res.status(403).json({
error: 'Forbidden',
message: 'User account is disabled'
});
}
// Attacher l'utilisateur à la requête
req.user = user;
req.token = decoded;
next();
} catch (error) {
if (error.message === 'Token expired') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Token expired',
code: 'TOKENEXPIRED'
});
}
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token'
});
}
};
/
Middleware pour vérifier les rôles
/
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required'
});
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions'
});
}
next();
};
};
module.exports = { authenticate, authorize };
// routes/auth.routes.js
const express = require('express');
const bcrypt = require('bcryptjs');
const router = express.Router();
const User = require('../models/User');
const RefreshToken = require('../models/RefreshToken');
const JWTUtil = require('../utils/jwt.util');
const { authenticate } = require('../middleware/auth.middleware');
/
POST /auth/register
Inscription d'un nouvel utilisateur
/
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Validation
if (!username || !email || !password) {
return res.status(400).json({
error: 'Validation failed',
details: {
username: !username ? 'Username is required' : null,
email: !email ? 'Email is required' : null,
password: !password ? 'Password is required' : null
}
});
}
// Vérifier si l'utilisateur existe
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(409).json({
error: 'Conflict',
message: 'User with this email already exists'
});
}
// Hash du mot de passe
const hashedPassword = await bcrypt.hash(password, 12);
// Créer l'utilisateur
const user = await User.create({
username,
email,
password: hashedPassword,
role: 'user'
});
// Générer les tokens
const accessToken = JWTUtil.generateAccessToken(user);
const refreshToken = JWTUtil.generateRefreshToken(user);
// Stocker le refresh token en base
await RefreshToken.create({
userid: user.id,
token: refreshToken,
expiresat: new Date(Date.now() + 7 24 60 60 1000) // 7 jours
});
res.status(201).json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
},
tokens: {
accesstoken: accessToken,
refreshtoken: refreshToken,
tokentype: 'Bearer',
expiresin: 900 // 15 minutes en secondes
}
});
} catch (error) {
console.error('Register error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/
POST /auth/login
Connexion d'un utilisateur
/
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validation
if (!email || !password) {
return res.status(400).json({
error: 'Validation failed',
message: 'Email and password are required'
});
}
// Trouver l'utilisateur
const user = await User.findOne({ where: { email } });
if (!user) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid credentials'
});
}
// Vérifier le mot de passe
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid credentials'
});
}
// Vérifier que le compte est actif
if (!user.isactive) {
return res.status(403).json({
error: 'Forbidden',
message: 'Account is disabled'
});
}
// Générer les tokens
const accessToken = JWTUtil.generateAccessToken(user);
const refreshToken = JWTUtil.generateRefreshToken(user);
// Stocker le refresh token
await RefreshToken.create({
userid: user.id,
token: refreshToken,
expiresat: new Date(Date.now() + 7 24 60 60 1000)
});
// Mettre à jour lastlogin
await user.update({ lastlogin: new Date() });
res.json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
},
tokens: {
accesstoken: accessToken,
refreshtoken: refreshToken,
tokentype: 'Bearer',
expiresin: 900
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/
POST /auth/refresh
Rafraîchir le access token avec un refresh token
/
router.post('/refresh', async (req, res) => {
try {
const { refreshtoken } = req.body;
if (!refreshtoken) {
return res.status(400).json({
error: 'Validation failed',
message: 'Refresh token is required'
});
}
// Vérifier le token
let decoded;
try {
decoded = JWTUtil.verifyToken(refreshtoken);
} catch (error) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or expired refresh token'
});
}
// Vérifier que c'est un refresh token
if (decoded.type !== 'refresh') {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token type'
});
}
// Vérifier que le refresh token existe en base
const storedToken = await RefreshToken.findOne({
where: {
token: refreshtoken,
userid: decoded.sub,
revoked: false
}
});
if (!storedToken) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Refresh token not found or revoked'
});
}
// Charger l'utilisateur
const user = await User.findByPk(decoded.sub);
if (!user || !user.isactive) {
return res.status(401).json({
error: 'Unauthorized',
message: 'User not found or inactive'
});
}
// Générer un nouveau access token
const accessToken = JWTUtil.generateAccessToken(user);
// Optionnel: Rotation du refresh token (plus sécurisé)
const newRefreshToken = JWTUtil.generateRefreshToken(user);
// Révoquer l'ancien refresh token
await storedToken.update({ revoked: true });
// Créer le nouveau refresh token
await RefreshToken.create({
userid: user.id,
token: newRefreshToken,
expiresat: new Date(Date.now() + 7 24 60 60 1000)
});
res.json({
tokens: {
accesstoken: accessToken,
refreshtoken: newRefreshToken,
tokentype: 'Bearer',
expiresin: 900
}
});
} catch (error) {
console.error('Refresh error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/
POST /auth/logout
Déconnexion (révocation du refresh token)
/
router.post('/logout', authenticate, async (req, res) => {
try {
const { refreshtoken } = req.body;
if (refreshtoken) {
// Révoquer le refresh token spécifique
await RefreshToken.update(
{ revoked: true },
{ where: { token: refreshtoken, userid: req.user.id } }
);
} else {
// Révoquer tous les refresh tokens de l'utilisateur
await RefreshToken.update(
{ revoked: true },
{ where: { userid: req.user.id, revoked: false } }
);
}
res.status(204).send();
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/
GET /auth/me
Récupérer l'utilisateur connecté
/
router.get('/me', authenticate, async (req, res) => {
res.json({
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email,
role: req.user.role,
createdat: req.user.createdat
}
});
});
module.exports = router;
// models/RefreshToken.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const RefreshToken = sequelize.define('RefreshToken', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userid: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
token: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
expiresat: {
type: DataTypes.DATE,
allowNull: false
},
revoked: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
tableName: 'refreshtokens',
timestamps: true,
underscored: true
});
module.exports = RefreshToken;
// routes/users.routes.js
const express = require('express');
const router = express.Router();
const { authenticate, authorize } = require('../middleware/auth.middleware');
// Route publique
router.get('/public', (req, res) => {
res.json({ message: 'Public route' });
});
// Route nécessitant une authentification
router.get('/profile', authenticate, (req, res) => {
res.json({
user: {
id: req.user.id,
username: req.user.username,
email: req.user.email
}
});
});
// Route réservée aux admins
router.get('/admin', authenticate, authorize('admin'), async (req, res) => {
const users = await User.findAll();
res.json({ users });
});
// Route multi-rôles
router.get('/moderator', authenticate, authorize('admin', 'moderator'), (req, res) => {
res.json({ message: 'Moderator or admin access' });
});
module.exports = router;
OAuth 2.0 définit plusieurs flux (flows) selon le contexte:
// Installation: npm install oauth2-server express-oauth-server
// models/oauth.model.js
const User = require('./User');
const OAuthClient = require('./OAuthClient');
const OAuthAccessToken = require('./OAuthAccessToken');
const OAuthRefreshToken = require('./OAuthRefreshToken');
const OAuthAuthorizationCode = require('./OAuthAuthorizationCode');
const bcrypt = require('bcryptjs');
class OAuthModel {
/
Récupérer un access token
/
async getAccessToken(accessToken) {
const token = await OAuthAccessToken.findOne({
where: { accesstoken: accessToken },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!token) return false;
return {
accessToken: token.accesstoken,
accessTokenExpiresAt: token.expiresat,
scope: token.scope,
client: { id: token.client.clientid },
user: { id: token.user.id }
};
}
/
Récupérer un client
/
async getClient(clientId, clientSecret) {
const client = await OAuthClient.findOne({
where: { clientid: clientId }
});
if (!client) return false;
// Vérifier le secret si fourni
if (clientSecret && client.clientsecret !== clientSecret) {
return false;
}
return {
id: client.clientid,
redirectUris: client.redirecturis,
grants: client.grants
};
}
/
Récupérer un refresh token
/
async getRefreshToken(refreshToken) {
const token = await OAuthRefreshToken.findOne({
where: { refreshtoken: refreshToken },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!token) return false;
return {
refreshToken: token.refreshtoken,
refreshTokenExpiresAt: token.expiresat,
scope: token.scope,
client: { id: token.client.clientid },
user: { id: token.user.id }
};
}
/
Récupérer un authorization code
/
async getAuthorizationCode(authorizationCode) {
const code = await OAuthAuthorizationCode.findOne({
where: { authorizationcode: authorizationCode },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!code) return false;
return {
code: code.authorizationcode,
expiresAt: code.expiresat,
redirectUri: code.redirecturi,
scope: code.scope,
client: { id: code.client.clientid },
user: { id: code.user.id }
};
}
/
Sauvegarder un token
/
async saveToken(token, client, user) {
const accessToken = await OAuthAccessToken.create({
accesstoken: token.accessToken,
expiresat: token.accessTokenExpiresAt,
scope: token.scope,
clientid: client.id,
userid: user.id
});
let refreshToken = null;
if (token.refreshToken) {
refreshToken = await OAuthRefreshToken.create({
refreshtoken: token.refreshToken,
expiresat: token.refreshTokenExpiresAt,
scope: token.scope,
clientid: client.id,
userid: user.id
});
}
return {
accessToken: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
refreshToken: token.refreshToken,
refreshTokenExpiresAt: token.refreshTokenExpiresAt,
scope: token.scope,
client: { id: client.id },
user: { id: user.id }
};
}
/
Sauvegarder un authorization code
/
async saveAuthorizationCode(code, client, user) {
const authCode = await OAuthAuthorizationCode.create({
authorizationcode: code.authorizationCode,
expiresat: code.expiresAt,
redirecturi: code.redirectUri,
scope: code.scope,
clientid: client.id,
userid: user.id
});
return {
authorizationCode: authCode.authorizationcode,
expiresAt: authCode.expiresat,
redirectUri: authCode.redirecturi,
scope: authCode.scope,
client: { id: client.id },
user: { id: user.id }
};
}
/
Révoquer un authorization code
/
async revokeAuthorizationCode(code) {
const deleted = await OAuthAuthorizationCode.destroy({
where: { authorizationcode: code.code }
});
return deleted > 0;
}
/
Révoquer un refresh token
/
async revokeToken(token) {
const deleted = await OAuthRefreshToken.destroy({
where: { refreshtoken: token.refreshToken }
});
return deleted > 0;
}
/
Obtenir l'utilisateur (Resource Owner Password Credentials)
/
async getUser(username, password) {
const user = await User.findOne({ where: { email: username } });
if (!user) return false;
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) return false;
return { id: user.id };
}
/
Vérifier le scope
/
async verifyScope(token, scope) {
if (!token.scope) return false;
const requestedScopes = scope.split(' ');
const authorizedScopes = token.scope.split(' ');
return requestedScopes.every(s => authorizedScopes.includes(s));
}
}
module.exports = new OAuthModel();
// server.js - Configuration OAuth
const express = require('express');
const OAuth2Server = require('oauth2-server');
const oauthModel = require('./models/oauth.model');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Initialiser OAuth2 Server
app.oauth = new OAuth2Server({
model: oauthModel,
accessTokenLifetime: 3600, // 1 heure
refreshTokenLifetime: 1209600, // 2 semaines
allowBearerTokensInQueryString: false,
allowExtendedTokenAttributes: true
});
// routes/oauth.routes.js
const express = require('express');
const router = express.Router();
const Request = require('oauth2-server').Request;
const Response = require('oauth2-server').Response;
/
POST /oauth/token
Obtenir un access token
/
router.post('/token', async (req, res) => {
const request = new Request(req);
const response = new Response(res);
try {
const token = await req.app.oauth.token(request, response);
res.json({
accesstoken: token.accessToken,
tokentype: 'Bearer',
expiresin: token.accessTokenExpiresAt
? Math.floor((token.accessTokenExpiresAt - new Date()) / 1000)
: null,
refreshtoken: token.refreshToken,
scope: token.scope
});
} catch (error) {
res.status(error.code || 500).json({
error: error.name,
errordescription: error.message
});
}
});
/
GET /oauth/authorize
Page d'autorisation
/
router.get('/authorize', async (req, res) => {
// Afficher la page d'autorisation
res.render('authorize', {
clientid: req.query.clientid,
redirecturi: req.query.redirecturi,
scope: req.query.scope,
state: req.query.state
});
});
/
POST /oauth/authorize
Autoriser une application
/
router.post('/authorize', async (req, res) => {
const request = new Request(req);
const response = new Response(res);
try {
const code = await req.app.oauth.authorize(request, response);
res.redirect(${code.redirectUri}?code=${code.authorizationCode}&state=${req.body.state});
} catch (error) {
res.status(error.code || 500).json({
error: error.name,
errordescription: error.message
});
}
});
/
Middleware pour authentifier les requêtes
/
const authenticate = () => {
return async (req, res, next) => {
const request = new Request(req);
const response = new Response(res);
try {
const token = await req.app.oauth.authenticate(request, response);
req.user = token.user;
req.scope = token.scope;
next();
} catch (error) {
res.status(error.code || 401).json({
error: error.name,
errordescription: error.message
});
}
};
};
module.exports = { router, authenticate };
// Client Node.js utilisant OAuth
const axios = require('axios');
const qs = require('querystring');
class OAuthClient {
constructor(clientId, clientSecret, tokenUrl) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenUrl = tokenUrl;
this.accessToken = null;
}
/
Authorization Code Flow
/
async getAuthorizationUrl(redirectUri, scope, state) {
const params = {
responsetype: 'code',
clientid: this.clientId,
redirecturi: redirectUri,
scope: scope,
state: state
};
return https://api.exemple.com/oauth/authorize?${qs.stringify(params)};
}
/
Échanger le code contre un token
/
async exchangeCodeForToken(code, redirectUri) {
try {
const response = await axios.post(
this.tokenUrl,
qs.stringify({
granttype: 'authorizationcode',
code: code,
redirecturi: redirectUri,
clientid: this.clientId,
clientsecret: this.clientSecret
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.accesstoken;
this.refreshToken = response.data.refreshtoken;
return response.data;
} catch (error) {
console.error('Token exchange error:', error.response?.data);
throw error;
}
}
/
Client Credentials Flow
/
async getClientCredentialsToken() {
try {
const response = await axios.post(
this.tokenUrl,
qs.stringify({
granttype: 'clientcredentials',
scope: 'read write'
}),
{
auth: {
username: this.clientId,
password: this.clientSecret
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.accesstoken;
return response.data;
} catch (error) {
console.error('Client credentials error:', error.response?.data);
throw error;
}
}
/
Rafraîchir le token
/
async refreshAccessToken() {
try {
const response = await axios.post(
this.tokenUrl,
qs.stringify({
granttype: 'refreshtoken',
refreshtoken: this.refreshToken,
clientid: this.clientId,
clientsecret: this.clientSecret
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.accesstoken;
return response.data;
} catch (error) {
console.error('Refresh token error:', error.response?.data);
throw error;
}
}
/
Faire une requête API authentifiée
/
async makeAuthenticatedRequest(url, method = 'GET', data = null) {
try {
const response = await axios({
method,
url,
data,
headers: {
Authorization: Bearer ${this.accessToken}
}
});
return response.data;
} catch (error) {
// Si 401, essayer de rafraîchir le token
if (error.response?.status === 401 && this.refreshToken) {
await this.refreshAccessToken();
// Réessayer la requête
return this.makeAuthenticatedRequest(url, method, data);
}
throw error;
}
}
}
// Utilisation
const client = new OAuthClient(
'my-client-id',
'my-client-secret',
'https://api.exemple.com/oauth/token'
);
// Authorization Code Flow
app.get('/login', async (req, res) => {
const authUrl = await client.getAuthorizationUrl(
'http://localhost:3000/callback',
'read write',
'random-state-string'
);
res.redirect(authUrl);
});
app.get('/callback', async (req, res) => {
const { code } = req.query;
const tokens = await client.exchangeCodeForToken(
code,
'http://localhost:3000/callback'
);
res.json(tokens);
});
// Client Credentials Flow
async function useClientCredentials() {
await client.getClientCredentialsToken();
const data = await client.makeAuthenticatedRequest(
'https://api.exemple.com/v1/data'
);
console.log(data);
}
// models/ApiKey.js
const { DataTypes } = require('sequelize');
const crypto = require('crypto');
const sequelize = require('../config/database');
const ApiKey = sequelize.define('ApiKey', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
userid: {
type: DataTypes.INTEGER,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Nom descriptif de la clé'
},
key: {
type: DataTypes.STRING(64),
allowNull: false,
unique: true
},
secret: {
type: DataTypes.STRING(64),
allowNull: true,
comment: 'Secret pour HMAC (optionnel)'
},
permissions: {
type: DataTypes.JSON,
defaultValue: [],
comment: 'Liste des permissions accordées'
},
ratelimit: {
type: DataTypes.INTEGER,
defaultValue: 1000,
comment: 'Requêtes par heure'
},
lastusedat: {
type: DataTypes.DATE,
allowNull: true
},
expiresat: {
type: DataTypes.DATE,
allowNull: true
},
isactive: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'apikeys',
timestamps: true,
underscored: true,
hooks: {
beforeCreate: (apiKey) => {
// Générer une clé aléatoire si non fournie
if (!apiKey.key) {
apiKey.key = crypto.randomBytes(32).toString('hex');
}
// Générer un secret si nécessaire
if (apiKey.secret === undefined) {
apiKey.secret = crypto.randomBytes(32).toString('hex');
}
}
}
});
/
Générer une nouvelle API key
/
ApiKey.generateKey = async function(userId, name, options = {}) {
return await ApiKey.create({
userid: userId,
name: name,
permissions: options.permissions || [],
ratelimit: options.ratelimit || 1000,
expiresat: options.expiresat || null
});
};
/
Vérifier une API key
/
ApiKey.verify = async function(key) {
const apiKey = await ApiKey.findOne({
where: {
key: key,
isactive: true
}
});
if (!apiKey) return null;
// Vérifier l'expiration
if (apiKey.expiresat && new Date() > apiKey.expiresat) {
return null;
}
// Mettre à jour lastusedat
await apiKey.update({ lastusedat: new Date() });
return apiKey;
};
module.exports = ApiKey;
// middleware/apiKey.middleware.js
const ApiKey = require('../models/ApiKey');
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('../config/redis');
/
Middleware d'authentification par API Key
/
const authenticateApiKey = async (req, res, next) => {
try {
// Extraire la clé API du header
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({
error: 'Unauthorized',
message: 'API key is required'
});
}
// Vérifier la clé
const key = await ApiKey.verify(apiKey);
if (!key) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid or expired API key'
});
}
// Attacher la clé à la requête
req.apiKey = key;
req.userId = key.userid;
next();
} catch (error) {
console.error('API Key auth error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
/
Middleware de vérification des permissions
/
const requirePermission = (...requiredPermissions) => {
return (req, res, next) => {
if (!req.apiKey) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Authentication required'
});
}
const userPermissions = req.apiKey.permissions || [];
const hasPermission = requiredPermissions.every(perm =>
userPermissions.includes(perm) || userPermissions.includes('')
);
if (!hasPermission) {
return res.status(403).json({
error: 'Forbidden',
message: 'Insufficient permissions',
required: requiredPermissions,
granted: userPermissions
});
}
next();
};
};
/
Rate limiting par API Key
/
const createApiKeyRateLimiter = () => {
return async (req, res, next) => {
if (!req.apiKey) {
return next();
}
const key = ratelimit:apikey:${req.apiKey.id};
const limit = req.apiKey.ratelimit;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, 3600); // 1 heure
}
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - current));
res.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + 3600);
if (current > limit) {
return res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded',
retryafter: 3600
});
}
next();
};
};
module.exports = {
authenticateApiKey,
requirePermission,
createApiKeyRateLimiter
};
// routes/apiKeys.routes.js
const express = require('express');
const router = express.Router();
const ApiKey = require('../models/ApiKey');
const { authenticate } = require('../middleware/auth.middleware');
/
GET /api-keys
Lister les API keys de l'utilisateur
/
router.get('/', authenticate, async (req, res) => {
try {
const apiKeys = await ApiKey.findAll({
where: { userid: req.user.id },
attributes: { exclude: ['secret'] } // Ne pas exposer le secret
});
res.json({ data: apiKeys });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
/
POST /api-keys
Créer une nouvelle API key
/
router.post('/', authenticate, async (req, res) => {
try {
const { name, permissions, ratelimit, expiresindays } = req.body;
const options = {
permissions: permissions || [],
ratelimit: ratelimit || 1000
};
if (expiresindays) {
options.expiresat = new Date(
Date.now() + expiresindays 24 60 60 1000
);
}
const apiKey = await ApiKey.generateKey(req.user.id, name, options);
res.status(201).json({
id: apiKey.id,
name: apiKey.name,
key: apiKey.key,
secret: apiKey.secret,
permissions: apiKey.permissions,
ratelimit: apiKey.ratelimit,
expiresat: apiKey.expiresat,
message: 'Save this key securely. It will not be shown again.'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
/
DELETE /api-keys/:id
Révoquer une API key
/
router.delete('/:id', authenticate, async (req, res) => {
try {
const apiKey = await ApiKey.findOne({
where: {
id: req.params.id,
userid: req.user.id
}
});
if (!apiKey) {
return res.status(404).json({ error: 'API key not found' });
}
await apiKey.update({ isactive: false });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;
// Utilisation dans les routes
const { authenticateApiKey, requirePermission, createApiKeyRateLimiter } = require('../middleware/apiKey.middleware');
app.use('/api/v1',
authenticateApiKey,
createApiKeyRateLimiter()
);
// Route nécessitant la permission 'read'
app.get('/api/v1/data',
requirePermission('read'),
async (req, res) => {
res.json({ data: 'Protected data' });
}
);
// Route nécessitant plusieurs permissions
app.post('/api/v1/data',
requirePermission('write', 'admin'),
async (req, res) => {
res.json({ message: 'Data created' });
}
);
// MAUVAIS - Jamais hardcoder les secrets
const JWTSECRET = 'mon-secret-123';
// BON - Utiliser des variables d'environnement
require('dotenv').config();
const JWTSECRET = process.env.JWTSECRET;
// MEILLEUR - Vérifier que le secret est défini
if (!process.env.JWTSECRET || process.env.JWTSECRET.length < 32) {
throw new Error('JWTSECRET must be defined and at least 32 characters');
}
// middleware/httpsRedirect.js
const enforceHTTPS = (req, res, next) => {
if (process.env.NODEENV === 'production' && !req.secure) {
return res.redirect(301, https://${req.headers.host}${req.url});
}
next();
};
app.use(enforceHTTPS);
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// Headers de sécurité
app.use(helmet());
// Rate limiting global
const limiter = rateLimit({
windowMs: 15 60 1000, // 15 minutes
max: 100, // 100 requêtes par IP
message: 'Too many requests from this IP'
});
app.use('/api/', limiter);
// Rate limiting sur le login
const loginLimiter = rateLimit({
windowMs: 15 60 1000,
max: 5,
message: 'Too many login attempts, try again later',
skipSuccessfulRequests: true
});
app.use('/auth/login', loginLimiter);
// models/TokenBlacklist.js
const redis = require('../config/redis');
class TokenBlacklist {
/
Ajouter un token à la blacklist
/
static async add(token, expiresAt) {
const ttl = Math.floor((expiresAt - Date.now()) / 1000);
if (ttl > 0) {
await redis.setex(blacklist:${token}, ttl, '1');
}
}
/
Vérifier si un token est blacklisté
/
static async isBlacklisted(token) {
const result = await redis.get(blacklist:${token});
return result !== null;
}
}
// Dans le middleware d'authentification
const authenticate = async (req, res, next) => {
try {
const token = extractToken(req);
const decoded = JWTUtil.verifyToken(token);
// Vérifier la blacklist
const isBlacklisted = await TokenBlacklist.isBlacklisted(token);
if (isBlacklisted) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Token has been revoked'
});
}
req.user = await User.findByPk(decoded.sub);
next();
} catch (error) {
// ...
}
};
// tests/auth.test.js
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
const bcrypt = require('bcryptjs');
describe('Authentication', () => {
let accessToken;
let refreshToken;
beforeAll(async () => {
// Créer un utilisateur de test
await User.create({
username: 'testuser',
email: 'test@exemple.com',
password: await bcrypt.hash('password123', 12),
role: 'user'
});
});
describe('POST /auth/login', () => {
it('devrait connecter un utilisateur valide', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'test@exemple.com',
password: 'password123'
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('tokens');
expect(res.body.tokens).toHaveProperty('accesstoken');
expect(res.body.tokens).toHaveProperty('refreshtoken');
accessToken = res.body.tokens.accesstoken;
refreshToken = res.body.tokens.refreshtoken;
});
it('devrait rejeter des identifiants invalides', async () => {
const res = await request(app)
.post('/auth/login')
.send({
email: 'test@exemple.com',
password: 'wrongpassword'
});
expect(res.statusCode).toBe(401);
});
});
describe('GET /auth/me', () => {
it('devrait retourner l'utilisateur connecté', async () => {
const res = await request(app)
.get('/auth/me')
.set('Authorization', Bearer ${accessToken});
expect(res.statusCode).toBe(200);
expect(res.body.user.email).toBe('test@exemple.com');
});
it('devrait rejeter sans token', async () => {
const res = await request(app)
.get('/auth/me');
expect(res.statusCode).toBe(401);
});
it('devrait rejeter un token invalide', async () => {
const res = await request(app)
.get('/auth/me')
.set('Authorization', 'Bearer invalid-token');
expect(res.statusCode).toBe(401);
});
});
describe('POST /auth/refresh', () => {
it('devrait rafraîchir le access token', async () => {
const res = await request(app)
.post('/auth/refresh')
.send({ refreshtoken: refreshToken });
expect(res.statusCode).toBe(200);
expect(res.body.tokens).toHaveProperty('accesstoken');
});
});
describe('POST /auth/logout', () => {
it('devrait révoquer le refresh token', async () => {
const res = await request(app)
.post('/auth/logout')
.set('Authorization', Bearer ${accessToken})
.send({ refreshtoken: refreshToken });
expect(res.statusCode).toBe(204);
// Le refresh token ne devrait plus fonctionner
const refreshRes = await request(app)
.post('/auth/refresh')
.send({ refreshtoken: refreshToken });
expect(refreshRes.statusCode).toBe(401);
});
});
});
La sécurisation des APIs en 2025 nécessite une compréhension approfondie des différentes méthodes d’authentification :
Points clés à retenir:
Mots-clés:** JWT, OAuth 2.0, API Keys, authentification API, sécurité API, Express.js, tokens, refresh tokens, API security, authentication middleware, rate limiting
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.