HappiHub

Gestion des Tokens JWT dans HappiHub

Table des Matières

Introduction à JWT

Qu’est-ce qu’un JWT ?

JSON Web Token (JWT) est un standard ouvert (RFC 7519) qui permet l’échange sécurisé de données entre parties sous forme d’un objet JSON. Chaque JWT est constitué de trois parties :

  1. Header (En-tête) : Contient le type de token (JWT) et l’algorithme de signature utilisé (par exemple, HMAC SHA256 ou RSA).
  2. Payload (Charge utile) : Contient les revendications. Les revendications sont des déclarations sur une entité (généralement, l’utilisateur) et des métadonnées supplémentaires.
  3. Signature (Signature) : Assure que le token n’a pas été modifié. Elle est créée en prenant le header, le payload, un secret, et en appliquant l’algorithme spécifié dans le header.

Un JWT ressemble à ceci :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Pourquoi utiliser JWT dans HappiHub ?

Génération de Tokens JWT

Processus de Génération

Pour générer un token JWT, nous utilisons une clé secrète pour signer le token. Voici un exemple de code utilisant Node.js et le module jsonwebtoken.

const jwt = require('jsonwebtoken');

function generateToken(user) {
    const payload = {
        id: user.id,
        email: user.email,
    };
    const secret = process.env.JWT_SECRET;
    const options = {
        expiresIn: '1h', // Le token expire dans 1 heure
    };

    return jwt.sign(payload, secret, options);
}

Description du Code

Exemple de Code

const user = { id: '12345', email: 'user@example.com' };
const token = generateToken(user);
console.log('Generated JWT:', token);

Dans cet exemple, nous créons un objet utilisateur avec un id et un email, puis nous générons un token JWT pour cet utilisateur et l’affichons dans la console.

Autres Exemples Possibles

  1. Génération de Tokens avec des Rôles Utilisateur :
function generateToken(user) {
    const payload = {
        id: user.id,
        email: user.email,
        role: user.role, // Ajout du rôle de l'utilisateur
    };
    const secret = process.env.JWT_SECRET;
    const options = {
        expiresIn: '2h', // Le token expire dans 2 heures
    };

    return jwt.sign(payload, secret, options);
}

const user = { id: '67890', email: 'admin@example.com', role: 'admin' };
const token = generateToken(user);
console.log('Generated JWT with role:', token);
  1. Génération de Tokens avec des Claims Additionnels :
function generateToken(user) {
    const payload = {
        id: user.id,
        email: user.email,
        permissions: user.permissions, // Ajout des permissions de l'utilisateur
        issuer: 'HappiHub', // Ajout de l'émetteur du token
    };
    const secret = process.env.JWT_SECRET;
    const options = {
        expiresIn: '3h', // Le token expire dans 3 heures
    };

    return jwt.sign(payload, secret, options);
}

const user = { id: '54321', email: 'user@example.com', permissions: ['read', 'write'] };
const token = generateToken(user);
console.log('Generated JWT with additional claims:', token);
  1. Génération de Tokens avec une Durée de Vie Personnalisée :
function generateToken(user, expiresIn) {
    const payload = {
        id: user.id,
        email: user.email,
    };
    const secret = process.env.JWT_SECRET;
    const options = {
        expiresIn: expiresIn || '1h', // Utilisation d'une durée de vie personnalisée ou par défaut à 1 heure
    };

    return jwt.sign(payload, secret, options);
}

const user = { id: '11223', email: 'user2@example.com' };
const token = generateToken(user, '6h');
console.log('Generated JWT with custom expiry:', token);

Liste des Claims Possibles

Claims Obligatoires (Mandatory)

Claims Optionnels (Optional)

Exemple d’Utilisation des Claims

Voici un exemple de génération de token JWT avec des claims obligatoires, recommandés et optionnels :

function generateToken(user) {
    const payload = {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        permissions: user.permissions,
        iss: 'HappiHub',
        sub: 'userAuthentication',
        aud: 'happihubClient',
        exp: Math.floor(Date.now() / 1000) + (60 * 60), // Expire dans 1 heure
        iat: Math.floor(Date.now() / 1000),
        jti: 'uniqueIdentifier123'
    };
    const secret = process.env.JWT_SECRET;
    const options = {};

    return jwt.sign(payload, secret, options);
}

const user = {
    id: '67890',
    email: 'admin@example.com',
    name: 'John Doe',
    role: 'admin',
    permissions: ['read', 'write', 'delete']
};

const token = generateToken(user);
console.log('Generated JWT with various claims:', token);

Validation de Tokens JWT

Comment Valider un Token JWT

Pour valider un token JWT, nous devons vérifier sa signature et s’assurer qu’il n’est pas expiré. Voici un exemple de code.

function validateToken(token) {
    const secret = process.env.JWT_SECRET;
    
    try {
        const decoded = jwt.verify(token, secret);
        return decoded;
    } catch (err) {
        console.error('Invalid token:', err);
        return null;
    }
}

Description du Code

Exemple de Code

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const decodedToken = validateToken(token);
if (decodedToken) {
    console.log('Token is valid:', decodedToken);
} else {
    console.log('Token is invalid');
}

Dans cet exemple, nous remplaçons token par un token JWT réel, validons le token et affichons le contenu décodé s’il est valide, ou un message d’erreur s’il est invalide.

Autres Exemples Possibles

  1. Validation de Tokens avec Expiration Personnalisée :
function validateTokenWithExpiration(token, maxAge) {
    const secret = process.env.JWT_SECRET;
    
    try {
        const decoded = jwt.verify(token, secret);
        const currentTime = Math.floor(Date.now() / 1000);

        if (decoded.exp < currentTime || (decoded.iat + maxAge) < currentTime) {
            console.error('Token is expired');
            return null;
        }
        return decoded;
    } catch (err) {
        console.error('Invalid token:', err);
        return null;
    }
}

const maxAge = 3600; // 1 heure en secondes
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const decodedToken = validateTokenWithExpiration(token, maxAge);
if (decodedToken) {
    console.log('Token is valid with custom expiration:', decodedToken);
} else {
    console.log('Token is invalid or expired');
}
  1. Validation de Tokens avec Rôles Utilisateur :
function validateTokenWithRole(token, requiredRole) {
    const secret = process.env.JWT_SECRET;
    
    try {
        const decoded = jwt.verify(token, secret);
        
        if (decoded.role !== requiredRole) {
            console.error('Invalid role');
            return null;
        }
        return decoded;
    } catch (err) {
        console.error('Invalid token:', err);
        return null;
    }
}

const requiredRole = 'admin';
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const decodedToken = validateTokenWithRole(token, requiredRole);
if (decodedToken) {
    console.log('Token is valid with role:', decodedToken);
} else {
    console.log('Token is invalid or role mismatch');
}
  1. Validation de Tokens avec Claims Spécifiques :
function validateTokenWithClaims(token, claims) {
    const secret = process.env.JWT_SECRET;
    
    try {
        const decoded = jwt.verify(token, secret);

        for (let key in claims) {
            if (decoded[key] !== claims[key]) {
                console.error(`Invalid claim: ${key}`);
                return null;
            }
        }
        return decoded;
    } catch (err) {
        console.error('Invalid token:', err);
        return null;
    }
}

const claims = { role: 'admin', permissions: ['read', 'write'] };
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const decodedToken = validateTokenWithClaims(token, claims);
if (decodedToken) {
    console.log('Token is valid with claims:', decodedToken);
} else {
    console.log('Token is invalid or claims mismatch');
}

Rafraîchissement des Tokens JWT

Pourquoi et Quand Rafraîchir un Token ?

Les tokens JWT ont une durée de vie limitée pour des raisons de sécurité. Un token expiré ne peut plus être utilisé pour accéder aux ressources protégées. Il est donc nécessaire de rafraîchir le token avant qu’il n’expire pour maintenir la session de l’utilisateur active. Le rafraîchissement des tokens permet de prolonger la durée de vie de la session de l’utilisateur sans exiger une nouvelle authentification complète.

Processus de Rafraîchissement

Voici comment rafraîchir un token avant qu’il n’expire.

function refreshToken(token) {
    const decoded = validateToken(token);
    if (decoded) {
        return generateToken(decoded);
    }
    return null;
}

Description du Code

Exemple de Code

const oldToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const newToken = refreshToken(oldToken);
if (newToken) {
    console.log('Token refreshed:', newToken);
} else {
    console.log('Failed to refresh token');
}

Dans cet exemple, nous remplaçons oldToken par un token JWT réel, tentons de le rafraîchir, et affichons le nouveau token s’il est rafraîchi avec succès, ou un message d’erreur s’il échoue.

Autres Exemples Possibles

  1. Rafraîchissement avec Contrôle de Durée de Vie Restante :
function refreshTokenWithCheck(token) {
    const decoded = validateToken(token);
    if (decoded) {
        const currentTime = Math.floor(Date.now() / 1000);
        const timeLeft = decoded.exp - currentTime;
        const minTimeLeft = 60 * 15; // Rafraîchir si le temps restant est inférieur à 15 minutes

        if (timeLeft < minTimeLeft) {
            return generateToken(decoded);
        } else {
            console.log('Token still has sufficient time left');
            return token;
        }
    }
    return null;
}

const oldToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const newToken = refreshTokenWithCheck(oldToken);
if (newToken) {
    console.log('Token refreshed or still valid:', newToken);
} else {
    console.log('Failed to refresh token');
}
  1. Rafraîchissement avec Informations Utilisateur Mises à Jour :
function refreshTokenWithUpdatedInfo(token, updatedUser) {
    const decoded = validateToken(token);
    if (decoded) {
        // Mettre à jour les informations utilisateur dans le payload
        decoded.email = updatedUser.email || decoded.email;
        decoded.role = updatedUser.role || decoded.role;

        return generateToken(decoded);
    }
    return null;
}

const oldToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const updatedUser = { email: 'newemail@example.com', role: 'admin' };
const newToken = refreshTokenWithUpdatedInfo(oldToken, updatedUser);
if (newToken) {
    console.log('Token refreshed with updated info:', newToken);
} else {
    console.log('Failed to refresh token');
}
  1. Rafraîchissement avec Notification de Rafraîchissement :
function refreshTokenWithNotification(token) {
    const decoded = validateToken(token);
    if (decoded) {
        const newToken = generateToken(decoded);
        if (newToken) {
            console.log('Token has been refreshed for user:', decoded.email);
            return newToken;
        }
    }
    return null;
}

const oldToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // Remplacer par un token réel
const newToken = refreshTokenWithNotification(oldToken);
if (newToken) {
    console.log('Token refreshed:', newToken);
} else {
    console.log('Failed to refresh token');
}

Utilisation de JWT à travers des Middlewares

Pourquoi Utiliser des Middlewares ?

Les middlewares permettent de gérer l’authentification et l’autorisation de manière centralisée, simplifiant ainsi la logique de sécurité et améliorant la maintenabilité du code. En utilisant des middlewares, vous pouvez appliquer des règles d’authentification et d’autorisation à toutes les requêtes ou à certaines routes spécifiques de manière uniforme et efficace.

Middleware d’Authentification

Ce middleware vérifie la présence d’un token JWT dans les en-têtes des requêtes et valide ce token.

const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) return res.sendStatus(401); // Si aucun token, retournez 401

    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // Si le token est invalide, retournez 403
        req.user = user;
        next(); // Passez au middleware ou route suivant
    });
}

Description du Code

Exemple d’Utilisation du Middleware

Appliquez le middleware à des routes spécifiques pour protéger ces routes.

const express = require('express');
const app = express();

app.get('/protected', authenticateToken, (req, res) => {
    res.send('This is a protected route');
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Dans cet exemple, nous utilisons Express.js pour créer un serveur et appliquons le middleware authenticateToken à la route /protected. Toute requête à cette route doit inclure un token JWT valide dans l’en-tête Authorization.

Autres Exemples Possibles

  1. Middleware d’Authentification avec Rôle Utilisateur :

Ce middleware vérifie non seulement la validité du token, mais également le rôle de l’utilisateur pour s’assurer qu’il a les permissions appropriées pour accéder à la route.

function authenticateTokenWithRole(role) {
    return (req, res, next) => {
        const authHeader = req.headers['authorization'];
        const token = authHeader && authHeader.split(' ')[1];

        if (token == null) return res.sendStatus(401); // Si aucun token, retournez 401

        jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
            if (err) return res.sendStatus(403); // Si le token est invalide, retournez 403
            if (user.role !== role) return res.sendStatus(403); // Si le rôle ne correspond pas, retournez 403
            req.user = user;
            next(); // Passez au middleware ou route suivant
        });
    };
}

app.get('/admin', authenticateTokenWithRole('admin'), (req, res) => {
    res.send('This is an admin route');
});
  1. Middleware d’Authentification avec Journaling :

Ce middleware enregistre chaque tentative de connexion pour des fins de journalisation et d’audit.

const fs = require('fs');

function authenticateTokenWithLogging(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) {
        fs.appendFileSync('access.log', `Unauthorized access attempt on ${new Date().toISOString()}\n`);
        return res.sendStatus(401); // Si aucun token, retournez 401
    }

    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) {
            fs.appendFileSync('access.log', `Forbidden access attempt on ${new Date().toISOString()}: ${err.message}\n`);
            return res.sendStatus(403); // Si le token est invalide, retournez 403
        }
        req.user = user;
        next(); // Passez au middleware ou route suivant
    });
}

app.get('/protected', authenticateTokenWithLogging, (req, res) => {
    res.send('This is a protected route');
});
  1. Middleware d’Authentification avec Rafraîchissement de Token :

Ce middleware vérifie la validité du token et rafraîchit le token si sa durée de vie est proche de l’expiration.

function authenticateAndRefreshToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) return res.sendStatus(401); // Si aucun token, retournez 401

    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // Si le token est invalide, retournez 403

        const currentTime = Math.floor(Date.now() / 1000);
        const tokenExpiry = user.exp;

        // Rafraîchir le token si moins de 15 minutes restantes
        if (tokenExpiry - currentTime < 900) {
            const newToken = generateToken(user);
            res.setHeader('Authorization', `Bearer ${newToken}`);
        }

        req.user = user;
        next(); // Passez au middleware ou route suivant
    });
}

app.get('/protected', authenticateAndRefreshToken, (req, res) => {
    res.send('This is a protected route with token refresh');
});

Bonnes Pratiques de Sécurité

Stockage Sécurisé des Tokens

Ne jamais stocker des tokens JWT dans le stockage local ou les cookies sans sécurisation. Utilisez httpOnly et secure pour les cookies.

Rotation des Clés

Changez régulièrement les clés secrètes pour minimiser les risques en cas de fuite. La rotation des clés permet de sécuriser les tokens même si une clé secrète précédente a été compromise.

Gestion des Tokens Expirés

Invalider les tokens expirés et forcer les utilisateurs à se reconnecter. Implémenter une liste de révocation pour gérer les tokens invalidés.

Exemples de Code

Voici des exemples pratiques de génération, validation et rafraîchissement de tokens JWT dans HappiHub.

Exemple Complet

const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const secret = process.env.JWT_SECRET;

// Génération de Token
function generateToken(user) {
    const payload = { id: user.id, email: user.email };
    return jwt.sign(payload, secret, { expiresIn: '1h' });
}

// Validation de Token
function validateToken(token) {
    try {
        return jwt.verify(token, secret);
    } catch (err) {
        return null;
    }
}

// Rafraîchissement de Token
function refreshToken(token) {
    const decoded = validateToken(token);
    return decoded ? generateToken(decoded) : null;
}

// Exemples d'utilisation
const user = { id: '12345', email: 'user@example.com' };
const token = generateToken(user);
console.log('Generated JWT:', token);

const decodedToken = validateToken(token);
if (decodedToken) {
    console.log('Token is valid:', decodedToken);
} else {
    console.log('Token is invalid');
}

const newToken = refreshToken(token);
if (newToken) {
    console.log('Token refreshed:', newToken);
} else {
    console.log('Failed to refresh token');
}

// Middleware d'Authentification
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) return res.sendStatus(401); // Si aucun token, retournez 401

    jwt.verify(token, secret, (err, user) => {
        if (err) return res.sendStatus(403); // Si le token est invalide, retournez 403
        req.user = user;
        next(); // Passez au middleware ou route suivant
    });
}

// Exemple d'utilisation du middleware
app.get('/protected', authenticateToken, (req, res) => {
    res.send('This is a protected route');
});

// Démarrer le serveur
app.listen(3000, () => {
    console.log('Server is running on port 3000');
});