Estimated reading time: 23 minutes
Introduction
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é.
Comprendre les différentes approches
Tableau comparatif
| 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 |
Quand utiliser chaque approche?
JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
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
)
Implémentation complète avec Express.js
// config/jwt.config.js
module.exports = {
secret: process.env.JWT_SECRET || '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.is_active) {
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: 'TOKEN_EXPIRED'
});
}
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({
user_id: user.id,
token: refreshToken,
expires_at: 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: {
access_token: accessToken,
refresh_token: refreshToken,
token_type: 'Bearer',
expires_in: 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.is_active) {
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({
user_id: user.id,
token: refreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
// Mettre à jour last_login
await user.update({ last_login: new Date() });
res.json({
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
},
tokens: {
access_token: accessToken,
refresh_token: refreshToken,
token_type: 'Bearer',
expires_in: 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 { refresh_token } = req.body;
if (!refresh_token) {
return res.status(400).json({
error: 'Validation failed',
message: 'Refresh token is required'
});
}
// Vérifier le token
let decoded;
try {
decoded = JWTUtil.verifyToken(refresh_token);
} 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: refresh_token,
user_id: 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.is_active) {
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({
user_id: user.id,
token: newRefreshToken,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json({
tokens: {
access_token: accessToken,
refresh_token: newRefreshToken,
token_type: 'Bearer',
expires_in: 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 { refresh_token } = req.body;
if (refresh_token) {
// Révoquer le refresh token spécifique
await RefreshToken.update(
{ revoked: true },
{ where: { token: refresh_token, user_id: req.user.id } }
);
} else {
// Révoquer tous les refresh tokens de l'utilisateur
await RefreshToken.update(
{ revoked: true },
{ where: { user_id: 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,
created_at: req.user.created_at
}
});
});
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
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id'
}
},
token: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
expires_at: {
type: DataTypes.DATE,
allowNull: false
},
revoked: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
}, {
tableName: 'refresh_tokens',
timestamps: true,
underscored: true
});
module.exports = RefreshToken;
Utilisation dans les routes protégées
// 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
Les flux OAuth 2.0
OAuth 2.0 définit plusieurs flux (flows) selon le contexte:
Authorization Code Flow – Applications web avec backend
Implicit Flow – SPA (déprécié en 2025)
Resource Owner Password Credentials – Applications de confiance
Client Credentials Flow – Server-to-server
Authorization Code Flow with PKCE – Applications mobiles et SPA (recommandé)
Implémentation OAuth 2.0 Server avec Express
// 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: { access_token: accessToken },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!token) return false;
return {
accessToken: token.access_token,
accessTokenExpiresAt: token.expires_at,
scope: token.scope,
client: { id: token.client.client_id },
user: { id: token.user.id }
};
}
/**
* Récupérer un client
*/
async getClient(clientId, clientSecret) {
const client = await OAuthClient.findOne({
where: { client_id: clientId }
});
if (!client) return false;
// Vérifier le secret si fourni
if (clientSecret && client.client_secret !== clientSecret) {
return false;
}
return {
id: client.client_id,
redirectUris: client.redirect_uris,
grants: client.grants
};
}
/**
* Récupérer un refresh token
*/
async getRefreshToken(refreshToken) {
const token = await OAuthRefreshToken.findOne({
where: { refresh_token: refreshToken },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!token) return false;
return {
refreshToken: token.refresh_token,
refreshTokenExpiresAt: token.expires_at,
scope: token.scope,
client: { id: token.client.client_id },
user: { id: token.user.id }
};
}
/**
* Récupérer un authorization code
*/
async getAuthorizationCode(authorizationCode) {
const code = await OAuthAuthorizationCode.findOne({
where: { authorization_code: authorizationCode },
include: [
{ model: User, as: 'user' },
{ model: OAuthClient, as: 'client' }
]
});
if (!code) return false;
return {
code: code.authorization_code,
expiresAt: code.expires_at,
redirectUri: code.redirect_uri,
scope: code.scope,
client: { id: code.client.client_id },
user: { id: code.user.id }
};
}
/**
* Sauvegarder un token
*/
async saveToken(token, client, user) {
const accessToken = await OAuthAccessToken.create({
access_token: token.accessToken,
expires_at: token.accessTokenExpiresAt,
scope: token.scope,
client_id: client.id,
user_id: user.id
});
let refreshToken = null;
if (token.refreshToken) {
refreshToken = await OAuthRefreshToken.create({
refresh_token: token.refreshToken,
expires_at: token.refreshTokenExpiresAt,
scope: token.scope,
client_id: client.id,
user_id: 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({
authorization_code: code.authorizationCode,
expires_at: code.expiresAt,
redirect_uri: code.redirectUri,
scope: code.scope,
client_id: client.id,
user_id: user.id
});
return {
authorizationCode: authCode.authorization_code,
expiresAt: authCode.expires_at,
redirectUri: authCode.redirect_uri,
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: { authorization_code: code.code }
});
return deleted > 0;
}
/**
* Révoquer un refresh token
*/
async revokeToken(token) {
const deleted = await OAuthRefreshToken.destroy({
where: { refresh_token: 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({
access_token: token.accessToken,
token_type: 'Bearer',
expires_in: token.accessTokenExpiresAt
? Math.floor((token.accessTokenExpiresAt - new Date()) / 1000)
: null,
refresh_token: token.refreshToken,
scope: token.scope
});
} catch (error) {
res.status(error.code || 500).json({
error: error.name,
error_description: error.message
});
}
});
/**
* GET /oauth/authorize
* Page d'autorisation
*/
router.get('/authorize', async (req, res) => {
// Afficher la page d'autorisation
res.render('authorize', {
client_id: req.query.client_id,
redirect_uri: req.query.redirect_uri,
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,
error_description: 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,
error_description: error.message
});
}
};
};
module.exports = { router, authenticate };
Utilisation OAuth côté client
// 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 = {
response_type: 'code',
client_id: this.clientId,
redirect_uri: 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({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: this.clientId,
client_secret: this.clientSecret
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.access_token;
this.refreshToken = response.data.refresh_token;
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({
grant_type: 'client_credentials',
scope: 'read write'
}),
{
auth: {
username: this.clientId,
password: this.clientSecret
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.access_token;
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({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
this.accessToken = response.data.access_token;
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);
}
API Keys
Implémentation API Keys
// 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
},
user_id: {
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'
},
rate_limit: {
type: DataTypes.INTEGER,
defaultValue: 1000,
comment: 'Requêtes par heure'
},
last_used_at: {
type: DataTypes.DATE,
allowNull: true
},
expires_at: {
type: DataTypes.DATE,
allowNull: true
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
}, {
tableName: 'api_keys',
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({
user_id: userId,
name: name,
permissions: options.permissions || [],
rate_limit: options.rate_limit || 1000,
expires_at: options.expires_at || null
});
};
/**
* Vérifier une API key
*/
ApiKey.verify = async function(key) {
const apiKey = await ApiKey.findOne({
where: {
key: key,
is_active: true
}
});
if (!apiKey) return null;
// Vérifier l'expiration
if (apiKey.expires_at && new Date() > apiKey.expires_at) {
return null;
}
// Mettre à jour last_used_at
await apiKey.update({ last_used_at: 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.user_id;
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.rate_limit;
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',
retry_after: 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: { user_id: 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, rate_limit, expires_in_days } = req.body;
const options = {
permissions: permissions || [],
rate_limit: rate_limit || 1000
};
if (expires_in_days) {
options.expires_at = new Date(
Date.now() + expires_in_days * 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,
rate_limit: apiKey.rate_limit,
expires_at: apiKey.expires_at,
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,
user_id: req.user.id
}
});
if (!apiKey) {
return res.status(404).json({ error: 'API key not found' });
}
await apiKey.update({ is_active: 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' });
}
);
Sécurité et meilleures pratiques
1. Stockage sécurisé des secrets
// MAUVAIS - Jamais hardcoder les secrets
const JWT_SECRET = 'mon-secret-123';
// BON - Utiliser des variables d'environnement
require('dotenv').config();
const JWT_SECRET = process.env.JWT_SECRET;
// MEILLEUR - Vérifier que le secret est défini
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be defined and at least 32 characters');
}
2. HTTPS obligatoire
// middleware/httpsRedirect.js
const enforceHTTPS = (req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.redirect(301, https://${req.headers.host}${req.url});
}
next();
};
app.use(enforceHTTPS);
3. Protection contre les attaques
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);
4. Blacklist de tokens (pour JWT)
// 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 d'authentification
// 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('access_token');
expect(res.body.tokens).toHaveProperty('refresh_token');
accessToken = res.body.tokens.access_token;
refreshToken = res.body.tokens.refresh_token;
});
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({ refresh_token: refreshToken });
expect(res.statusCode).toBe(200);
expect(res.body.tokens).toHaveProperty('access_token');
});
});
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({ refresh_token: refreshToken });
expect(res.statusCode).toBe(204);
// Le refresh token ne devrait plus fonctionner
const refreshRes = await request(app)
.post('/auth/refresh')
.send({ refresh_token: refreshToken });
expect(refreshRes.statusCode).toBe(401);
});
});
});
Conclusion
La sécurisation des APIs en 2025 nécessite une compréhension approfondie des différentes méthodes d'authentification :
JWT pour les applications stateless et les microservices
OAuth 2.0 pour la délégation d'accès et les intégrations tierces
API Keys pour les intégrations server-to-server
Points clés à retenir:
Toujours utiliser HTTPS en production
Implémenter le rate limiting
Stocker les secrets de manière sécurisée
Utiliser des refresh tokens pour JWT
Implémenter la révocation de tokens
Tester exhaustivement l'authentification
Monitorer les tentatives de connexion
Utiliser des algorithmes cryptographiques forts
Ressources supplémentaires
JWT Best Practices
OAuth 2.0 RFC 6749
OWASP API Security Top 10
OAuth 2.0 Security Best Current Practice
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
Related Articles

Nextcloud installé dans un LXC Proxmox, mais lent et sans optimisations. Voici ce que j'ai fait pour passer d'une instance poussive à un setup qui tient la route : Redis, APCu, preview generator, Collabora, NFS et maintenance automatisée.
Grafana m'a réveillé un mardi matin avec une alerte "CPU > 85% sur heighliner". L'alerte…
Depuis un hôtel en déplacement, j'ai voulu me connecter à l'interface Proxmox de heighliner pour…
Articles similaires
· · ·
À propos de l'auteur
Pascal
Pascal — SRE & Observability Engineer. J'applique les pratiques SRE (SLI/SLO, alerting, observabilité) à mon homelab Proxmox, avec les mêmes exigences qu'en production.