Tuto Authentification/Refresh JSON Web Token en Nodejs avec Express

Auth/Refresh token JWT en Nodejs avec Express

JWT schéma explicatif

Schéma basique d'une authentification et authorization via JWT

Génération du JWT

Setup du projet node :

npm init

Le fichier par défaut devra être appelé app.js. Et dans le package.json créons une commande par defaut :

node app.js

Installons le package npm [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)

npm install jsonwebtoken

Puis l'importer dans les fichiers

const jwt = require('jsonwebtoken')

Pour créer le token on a besoin de 3 éléments :

  1. Une clé secrète
  2. Les données que l'on veut stocker dans le token
  3. La date d'expiration du token

Pour ne pas exposer la clé secrète dans son code il vaut mieux stocker la clé dans un fichier d'env.

Créeons notre fichier .env :

ACCESS_TOKEN_SECRET=4242XX424208
REFRESH_TOKEN_SECRET=424200000X1

Faites bien attention de générer une string aléatoire complexe

Pour utiliser notre fichier de conf on install le package NPM [dotenv](https://www.npmjs.com/package/dotenv) :

npm install dotenv

Et on l'utilise comme ça :

// chargement du fichier d'env
require('dotenv').config();
// accès au variables
process.env.ACCESS_TOKEN_SECRET;

Très bien, on a tout ce qu'il faut pour générer notre JWT

function generateAccessToken(user) {
  return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '1800s' });
}

const user = {
    id: 42,
    name: 'Jean Bon',
    email: 'jeanbon@gmail.com',
    admin: true,
};

const accessToken = generateAccessToken(user);
console.log('accessToken', accessToken);

Création des routes avec Express

Installons la le package npm [express](https://www.npmjs.com/package/express) :

npm install express
const express = require('express');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post('/api/login', (req, res) => {

    // TODO: fetch le user depuis la db basé sur l'email passé en paramètre
    if (req.body.email !== 'jeanbon@gmail.com') {
        res.status(401).send('invalid credentials');
        return ;
    }
    // TODO: check que le mot de passe du user est correct
    if (req.body.password !== 'cuillere') {
        res.status(401).send('invalid credentials');
        return ;
    }

  const accessToken = generateAccessToken(user);
  res.send({
        accessToken,
    });

});

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

Pour tester vous pouvez soit utiliser Postman ou si vous êtes sous VS Code, utiliser l'extension ThunderClient

Authorization et Authentification d'appels API

Pour ne pas devoir checker le token sur chaque route qui nécessite que l'utilisateur soit authentifié on va créer un middleware.

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

  if (token == null) return res.sendStatus(401)

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.sendStatus(401)
    }
    req.user = user;
    next();
  });
}

app.get('/api/me', authenticateToken, (req, res) => {
  res.send(req.user);
});

Vos appels sont désormais sécurisés via l'utilisation d'un JWT.

Refresh Token

Pour ne pas devoir se reconnecter sur une longue période de temps, tout en gardant la possibilité de ban ou de changer les droits d'un utilisateur sur cette période, il est important de mettre en place un mécanisme pour regénérer automatiquement le token.

On génére un refreshToken d'une durée bien plus longue.

function generateRefreshToken(user) {
  return jwt.sign(user, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '1y' });
}

const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
res.send({
    accessToken,
  refreshToken,
});

Puis on crée la route qui re-génère le token à partir du refreshToken.

app.post('/api/refreshToken', (req, res) => {
  const authHeader = req.headers['authorization']
  const token = authHeader && authHeader.split(' ')[1]

  if (token == null) return res.sendStatus(401)

  jwt.verify(token, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.sendStatus(401)
    }

    // TODO: Check en base que l'user est toujours existant/autorisé à utiliser la plateforme
    delete user.iat;
    delete user.exp;
    const refreshedToken = generateAccessToken(user);
    res.send({
      accessToken: refreshedToken,
    });
  });
});

Voilà vous avez une API NodeJS avec Authentification JWT !

Utilisation côté client avec Axios

On installe la librairie Axios.

npm install --save-dev axios

Notez que je l'installe uniquement en dev parce que c'est juste pour faire des tests sur le back-end. Pour votre front en Vue, React, autre, ne faites pas ça !

On créer un fichier fake-client.js qui va contenir notre implémentation axios. Je ne le fais pas sous la forme test-unitaire pour que vous puissiez exploiter le code dans votre projet facilement.

const axios = require('axios');
const instance = axios.create({
  baseURL: 'http://localhost:3000/api/',
});

let refreshToken;
// Todo: Load de l'accessToken depuis le localStorage et set du header

console.log('trying to auth');
instance.post('/login', {
  email: 'jeanbon@gmail.com',
  password: 'cuillere',
}).then((response) => {
  console.log('auth success');

    // Todo: Store de l'accessToken et du refreshToken dans le localStorage (ou cookie)

    instance.defaults.headers.common['Authorization'] = `Bearer ${response.data.accessToken}`;
  refreshToken = response.data.refreshToken;
  loadUserInfos();
}).catch((err) => {
  console.log(err.response.status);
});

function loadUserInfos() {
  instance.get('/me').then((response) => {
    console.log(response.data);
  }).catch((err) => {
    console.log(err.response.status);
  });
}

Définissez le script test pour qu'il appelle le fichier fake-client.js

{
  "name": "jwt-tuto",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "serve": "node app.js",
    "test": "node fake-client.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^10.0.0",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1"
  },
  "devDependencies": {
    "axios": "^0.21.1"
  }
}

L'authentification de l'utilisateur et l'affichage de ses informations fonctionnent bien. Maintenant on va altérer le token pour montrer comment mettre en place le mécanisme de refresh de token.

//instance.defaults.headers.common['Authorization'] = `Bearer ${response.data.accessToken}`;
/*
** Alter token to generate issue
*/
instance.defaults.headers.common['Authorization'] = `Bearer wrong`;

Puis on crée un interceptor pour appeler refreshToken quand on reçoit le status 401 (on le fait uniquement si on a un refreshToken et qu'une seule fois pour ne pas faire une boucle infinie avec le back-end)

instance.interceptors.response.use((response) => {
  return response
}, async function (error) {
  const originalRequest = error.config;
  if (error.config.url != "/refreshToken" && error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;
    if (refreshToken && refreshToken != "") {
      instance.defaults.headers.common['Authorization'] = `Bearer ${refreshToken}`;
      console.log('refreshToken');
      await instance.post('/refreshToken').then((response) => {
        // TODO: mettre à jour l'accessToken dans le localStorage
        originalRequest.headers['Authorization'] = `Bearer ${response.data.accessToken}`;
        instance.defaults.headers.common['Authorization'] = `Bearer ${response.data.accessToken}`;
      }).catch((err) => {
        console.log(err.response.status);
        refreshToken = null;
      });
      return instance(originalRequest);
    }
  }
  return Promise.reject(error);
});

Le lien vers le code source https://github.com/wass08/tuto-jwt-express