Intermediaire 21 min de lecture · 4 528 mots

Authentification API : JWT, OAuth 2.0, et API Keys

Estimated reading time: 22 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:

  • APIs RESTful stateless
  • Microservices
  • Applications SPA (Single Page Applications)
  • Mobile apps avec backend API
  • OAuth 2.0:

  • Applications tierces accédant à vos données
  • SSO (Single Sign-On)
  • « Login with Google/Facebook »
  • APIs publiques avec scopes granulaires
  • API Keys:

  • Intégrations server-to-server
  • Webhooks
  • Services internes
  • Rate limiting et identification
  • JSON Web Tokens (JWT)

    Structure d’un JWT

    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
)

Implémentation complète avec Express.js

// 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;

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: { 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 };
    

    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 = {
          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);
    }
    

    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
      },
      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' });
      }
    );
    

    Sécurité et meilleures pratiques

    1. Stockage sécurisé des secrets

    // 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');
    }
    

    2. HTTPS obligatoire

    // 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);
    

    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('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);
        });
      });
    });
    

    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

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.