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.
Manuel
Installer le module pour créer et valider les jetons
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
-
Créer variable de collection.
-
Créer une requête GET pour obtenir un jeton.
-
Dans Script, ajouter le code suivant pour conserver le jeton dans une variable d'environnement.
-
Dans la collection, ajouter la variable dans l'auth Bearer Token
-
Créer une requête GET pour obtenir les données.
-
Exécuter la requête de génération avant celle du GET pour obtenir les données.