Aller au contenu

Express et JWT

JWT (JSON Web Token) permet d’authentifier un utilisateur lors de chaque appel d'un point d'interface logicielle.

  • Le jeton est généré par le serveur, qui s’assure que l’utilisateur est bien celui qu’il prétend.
  • Le jeton est envoyé au client et ce dernier le conserve dans un témoin pour l’utiliser à chaque appel d'un point d'interface logicielle.
  • Un intergiciel du côté du serveur valide chaque appel d'un point d'interface logicielle en vérifiant le jeton.
  • Si le jeton n’est pas valide, le point d'interface logicielle ne retourne pas de données.

Installer le module pour créer et valider les jetons

console
npm install jsonwebtoken @types/jsonwebtoken

La variable d'environnement dans dev

config/.env.development
## Environment ##
NODE_ENV=development


## Server ##
PORT=3000
HOST=localhost

## JWT ##

JWTSECRET = "7ds8f97ds897f89sd7fsd87f89ds789f7d9s"

## Setup jet-logger ##
JET_LOGGER_MODE=CONSOLE
JET_LOGGER_FILEPATH=jet-logger.log
JET_LOGGER_TIMESTAMP=TRUE
JET_LOGGER_FORMAT=LINE

Exposer la variable dans ENV

src/common/constants/ENV.ts
import jetEnv, { num, str } from 'jet-env';
import { isEnumVal } from 'jet-validators';

import { NodeEnvs } from '.';

/******************************************************************************
                                 Setup
******************************************************************************/

const ENV = jetEnv({
  NodeEnv: isEnumVal(NodeEnvs),
  Port: num,
  Jwtsecret: str,
});

/******************************************************************************
                            Export default
******************************************************************************/

export default ENV;

Passer à ES2022

/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "lib": ["ES2022"],
    "strict": true,
    "baseUrl": "./",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "paths": {
      "@src/*": ["src/*"]
    },
    "useUnknownInCatchVariables": false,
    "types": ["vitest/globals"]
  },
  "ts-node": {
    "swc": true,
    "require": ["tsconfig-paths/register", "./config.ts"]
  },
  "include": [
    "src/**/*.ts",
    "tests/**/*.ts",
    "config.ts",
    "scripts",
    "eslint.config.ts",
    "vitest.config.mts"
  ],
  "exclude": ["src/public/*"]
}

Le modèle User (Ajouter UserLogin)

src/models/User.ts
import { isString } from 'jet-validators';
import { parseObject, TParseOnError } from 'jet-validators/utils';

import { isRelationalKey, transIsDate } from '@src/common/util/validators';
import { IModel } from './common/types';

/******************************************************************************
                                 Constants
******************************************************************************/

const DEFAULT_USER_VALS = (): IUser => ({
  id: -1,
  name: '',
  created: new Date(),
  password: '',
  email: '',
});

/******************************************************************************
                                  Types
******************************************************************************/

export interface IUser extends IModel {
  name: string;
  email: string;
  password: string;
}

export interface IUserLogin {
  email: string;
  password: string;
}

/******************************************************************************
                                  Setup
******************************************************************************/

// Initialize the "parseUser" function
const parseUser = parseObject<IUser>({
  id: isRelationalKey,
  name: isString,
  email: isString,
  password: isString,
  created: transIsDate,
});

// Initialize the "parseUserLogin" function
const parseUserLogin = parseObject<IUserLogin>({
  email: isString,
  password: isString,
});

/******************************************************************************
                                 Functions
******************************************************************************/

/**
 * New user object.
 */
function __new__(user?: Partial<IUser>): IUser {
  const retVal = { ...DEFAULT_USER_VALS(), ...user };
  return parseUser(retVal, (errors) => {
    throw new Error('Setup new user failed ' + JSON.stringify(errors, null, 2));
  });
}

/**
 * Check is a user object. For the route validation.
 */
function test(arg: unknown, errCb?: TParseOnError): arg is IUser {
  return !!parseUser(arg, errCb);
}

/**
 * Check is a user login object. For the route validation.
 */
function testlogin(arg: unknown, errCb?: TParseOnError): arg is IUserLogin {
  return !!parseUserLogin(arg, errCb);
}

/******************************************************************************
                                Export default
******************************************************************************/

export default {
  new: __new__,
  test,
  testlogin,
} as const;

Le service de génération de jetons

src/services/JetonService.ts
// **** Variables **** //

import { IUserLogin } from '@src/models/User';
import UserService from './UserService';
import jwt from 'jsonwebtoken';
import ENV from '@src/common/constants/ENV';

export const UTILISATEUR_NOT_FOUND_ERR = 'Utilisateur non trouvé';

// **** Functions **** //

/**
 * Générer un jeton pour un utilisateur
 *
 * @param {IUserLogin} utilisateur - L'utilisateur demandant le jeton
 * @returns {Promise} - Le jeton signé
 */
async function generateToken(utilisateur: IUserLogin): Promise<string> {
  const utilisateurBD = (await UserService.getAll()).filter(
    (user) => user.email === utilisateur.email,
  )[0];
  if (utilisateurBD && utilisateurBD.password === utilisateur.password) {
    return jwt.sign(utilisateur.email, ENV.Jwtsecret as string);
  } else {
    return '';
  }
}

// **** Export default **** //
export default {
  generateToken,
} as const;

Le chemin pour les jetons

src/common/constants/Paths.ts
export default {
  Base: '/api',
  GenerateToken: {
    Base: '/generatetoken',
    Get: '/',
  },
  Users: {
    Base: '/users',
    Get: '/all',
    Add: '/add',
    Update: '/update',
    Delete: '/delete/:id',
  },
} as const;

La route

src/routes/JetonRoutes.ts
import JetonService from '@src/services/JetonService';
import User from '@src/models/User';
import { IReq, IRes } from './common/types';
import { parseReq } from './common/util';
// **** Functions **** //

/******************************************************************************
                                Constants
******************************************************************************/

const Validators = {
  generatetoken: parseReq({ userLogin: User.testlogin }),
} as const;

/**
 * Générer un jeton.
 *
 * @param {IReq} req - La requête au serveur
 * @param {IRes} res - La réponse du serveur
 */
async function generateToken(req: IReq, res: IRes) {
  const { userLogin } = Validators.generatetoken(req.body);
  const token = await JetonService.generateToken(userLogin);
  return res.send({ token: token });
}

// **** Export default **** //

export default {
  generateToken,
} as const;

Le Router

src/routes/index.ts
import { Router } from 'express';

import Paths from '@src/common/constants/Paths';
import UserRoutes from './UserRoutes';
import JetonRoutes from './JetonRoutes';

/******************************************************************************
                                Setup
******************************************************************************/

const apiRouter = Router();

// Init token router
const tokenRouter = Router();

// Generate Token
tokenRouter.get(Paths.GenerateToken.Get, JetonRoutes.generateToken);

// ** Add tokenRouter ** //
apiRouter.use(Paths.GenerateToken.Base, tokenRouter);

// Init router
const userRouter = Router();

// Get all users
userRouter.get(Paths.Users.Get, UserRoutes.getAll);
userRouter.post(Paths.Users.Add, UserRoutes.add);
userRouter.put(Paths.Users.Update, UserRoutes.update);
userRouter.delete(Paths.Users.Delete, UserRoutes.delete);

// Add UserRouter
apiRouter.use(Paths.Users.Base, userRouter);

/******************************************************************************
                                Export default
******************************************************************************/

export default apiRouter;

L'intergiciel pour valider les jetons

src/services/authenticateToken.ts
import jwt from 'jsonwebtoken';
import { Response, Request, NextFunction } from 'express';
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
import ENV from '@src/common/constants/ENV';
/**
 * Intergiciel pour authentifier le jeton de l'utilisateur
 *
 * @param {Request} req - La requête au serveur
 * @param {Response} res - La réponse du serveur
 * @param {NextFunction} next - La fonction a appeler pour continuer le processus.
 */
function authenticateToken(req: Request, res: Response, next: NextFunction) {
  // Ne pas vérifier le token si l'url est celui de generatetoken
  const lastPartOfUrl = req.url.split('/').at(-1);
  if (lastPartOfUrl === 'generatetoken') {
    next();
    return;
  }

  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  console.log(token);

  if (token == null) return res.sendStatus(HttpStatusCodes.UNAUTHORIZED);

  jwt.verify(token, ENV.Jwtsecret as string, (err: any, user: any) => {
    console.log(err);

    if (err) return res.sendStatus(HttpStatusCodes.FORBIDDEN);

    next();
  });
}

export default authenticateToken;

Ajouter l'intergiciel au serveur

src/server.ts
import morgan from 'morgan';
import path from 'path';
import helmet from 'helmet';
import express, { Request, Response, NextFunction } from 'express';
import logger from 'jet-logger';

import BaseRouter from '@src/routes';

import Paths from '@src/common/constants/Paths';
import ENV from '@src/common/constants/ENV';
import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
import { RouteError } from '@src/common/util/route-errors';
import { NodeEnvs } from '@src/common/constants';
import authenticateToken from './services/authenticateToken';

/******************************************************************************
                                Setup
******************************************************************************/

const app = express();

// **** Middleware **** //

// Basic middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Show routes called in console during development
if (ENV.NodeEnv === NodeEnvs.Dev) {
  app.use(morgan('dev'));
}

// Security
if (ENV.NodeEnv === NodeEnvs.Production) {
  // eslint-disable-next-line n/no-process-env
  if (!process.env.DISABLE_HELMET) {
    app.use(helmet());
  }
}

app.use(authenticateToken);

// Add APIs, must be after middleware
app.use(Paths.Base, BaseRouter);

// Add error handler
app.use((err: Error, _: Request, res: Response, next: NextFunction) => {
  if (ENV.NodeEnv !== NodeEnvs.Test.valueOf()) {
    logger.err(err, true);
  }
  let status = HttpStatusCodes.BAD_REQUEST;
  if (err instanceof RouteError) {
    status = err.status;
    res.status(status).json({ error: err.message });
  }
  return next(err);
});

// **** FrontEnd Content **** //

// Set views directory (html)
const viewsDir = path.join(__dirname, 'views');
app.set('views', viewsDir);

// Set static directory (js and css).
const staticDir = path.join(__dirname, 'public');
app.use(express.static(staticDir));

// Nav to users pg by default
app.get('/', (_: Request, res: Response) => {
  return res.redirect('/users');
});

// Redirect to login if not logged in.
app.get('/users', (_: Request, res: Response) => {
  return res.sendFile('users.html', { root: viewsDir });
});

/******************************************************************************
                                Export default
******************************************************************************/

export default app;

Configurer Bruno pour utiliser les jetons

  1. Créer variable de collection.
    Bruno - Créer une variable de collection

  2. Créer une requête GET pour obtenir un jeton. Bruno - Créer une requête GET pour obtenir un jeton

  3. Dans Script, ajouter le code suivant pour conserver le jeton dans une variable d'environnement.

    bru.setEnvVar("jwttoken", res.body.token)
    
  4. Dans la collection, ajouter la variable dans l'auth Bearer Token
    Bruno - Créer une requête GET pour obtenir un jeton

  5. Créer une requête GET pour obtenir les données. Bruno - Ajouter le jeton à la requête GET

  6. Exécuter la requête de génération avant celle du GET pour obtenir les données.