Poursuivons l’article sur API REST avec sequelize
Dans cet article, nous allons ajouter une sécurité à notre application. Il y aura une url /api/user/login appelée en POST et permettant de se connecter grâce au couple identifiant/mot de passe. Une fois connecté, on affichera, les informations accessibles uniquement par un utilisateur connecté. On ajoutera également une url accessible uniquement par un utilisateur ayant un role de “SUPER_ADMIN”.
On installe les packages utiles.
npm i jsonwebtoken bcryptjs cors passport-jwt passport passport-local locutus --save
- bcryptjs : sert à “hasher” le mot de passe. Il permettra de vérifier si le “hashage” du mot de passe saisi correspond à celui enregistré dans la base de données.
- cors: (Cross Origin Resource Sharing) ajoutera une entête à l’URL et permettra à une url externe de lire correctement les informations sur l’URL appelé. C’est d’ailleurs un des objectifs de la création d’une API rest.
- passport-*: permet de gérer l’authentification. Dans notre exemple, on utilisera essentiellement passport-jwt et passport-local. En effet, il y a autant de possibilités offertes par cette librairies comme l’authentification facebook, ou google par exemple…
- locutus: est un package javascript qui permet d’utiliser l’ensemble des fonctions php en javascript. Nous aurions besoin de ce package pour utiliser la fonction php unserialize notamment pour récupérer le rôle de l’utilisateur connecté .
On modifie le fichier models/fos_user.js . C’est le “model” qui décrit la table de nos utilisateurs.
Au début du script
// Utile pour hasher le mot de passe
var bcrypt = require("bcryptjs");
Ajoutons ceci à la fin de la fonction (sequelize, DataTypes)
User.prototype.validPassword = function(password) {
return bcrypt.compareSync(password, this.password);
};
return User;
Cette fonction validPassword sert à comparer le “hash” du mot de passe saisi par l’utilisateur au mot de passe de ce même utilisateur enregistré dans la base de données.
JWT (Json Web Tokens)
Créons le fichier config/middleware/authenticate.js et ajoutons ce code
var jwt = require('jsonwebtoken');
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
var passport = require('passport');
var db = require("../../models");
var unserializephp = require('locutus/php/var/unserialize');
Configuration jwt
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = '12541302154877';
On peut choisir n’importe quelle chaîne dans secretOrKey. Il faut juste garder la même valeur quand on utilisera cette propriété un peu plus bas.
exports.jwtPassport = passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
db.fos_user.findOne({
where: {
id: jwt_payload._id
}
}).then(
(user) => {
if (!user) {
return done(null, false);
}
return done(null, user);
});
}));
verifyUser
La fonction verifyUser suivante permettra de savoir si un utilisateur est connecté ou non. Elle utilisera le passport-jwt pour déterminer cela.
exports.verifyUser = passport.authenticate('jwt', {session: false});
getToken
La fonction getToken suivante récupèrera le token donné lorsque la connexion est réussie. C’est ici qu’il faudra faire attention à ce que la valeur soit identique à la propriété secretOrKey définie plus haut.
expiresIn: 3600 → indique que la connexion est valide pour 3600 secondes -c’est-à-dire 1 heure. Au delà d’1 heure, il faudra demander un nouveau token (donc se connecter à nouveau) pour pouvoir être authentifié.
exports.getToken = function (user) {
return jwt.sign(user, '12541302154877', {
expiresIn: 3600
});
};
verifyAdmin
Cette fonction servira à détecter si l’utilisateur connecté a bien un rôle ROLE_SUPER_ADMIN . Exemple de cas : seul l’utilisateur ayant ce rôle aura le droit de supprimer une entrée dans la base de données. C’est dans cette fonction qu’on utilisera la fonction unserialize. Cela sera le cas uniquement pour notre exemple. En effet, le role est un champ contenant une donnée sérializée comme ci-dessous
A chacun d’adapter son code selon son propre besoin donc…
exports.verifyAdmin = (req, res, next) => {
let roles = unserializephp(req.user.roles);
let i = 0;
if (roles) {
roles.forEach((role) => {
if (['ROLE_SUPER_ADMIN'].includes(role)) {
i++;
}
})
}
if (i === 0) {
return res.json({
error: true,
message: "You are not admin",
roles: role
});
}
next();
};
Code complet: https://github.com/rabehasy/nodejs-express/blob/step3/config/middleware/authenticate.js
Route vers la page de connexion
Passons maintenant aux choses plus visibles et concrètes.
Modifions le fichier /app.js. On ajoute le controller responsable de la “route”
var apiUserRestRouter = require('./routes/rest/user');
Puis l’URL qu’on appellera pour établir les connexions et récupérer les informations de l’utilisateur.
app.use('/api/user', apiUserRestRouter);
Créons tout d’abord le fichier /config/passport.js et ajoutons les informations suivantes.
var passport = require("passport");
var LocalStrategy = require("passport-local").Strategy;
var db = require("../models");
passport.use(new LocalStrategy(
(username, password, done) => {
db.fos_user.findOne({
where: {
username: username
}
}).then(
(user) => {
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
passport.serializeUser(function(user, cb) {
cb(null, user);
});
passport.deserializeUser(function(obj, cb) {
cb(null, obj);
});
module.exports = passport;
Créons ensuite le fichier /routes/rest/user.js
var express = require('express');
var authenticate = require('../../config/middleware/authenticate')
var passport = require('../../config/passport')
const db = require('../../models');
var router = express.Router();
Login
Il s’agit d’une url appelée en POST. Elle demande en paramètre les champs username et password comme définis dans /config/passport.js
new LocalStrategy(
(username, password, done) => {
/**
* @POST /api/user/login
*/
router.post('/login', function(req, res, next) {
passport.authenticate('local', {session: false}, (err, user, info) => {
// console.log(user);
if (err || !user) {
return res.status(400).json({
message: 'Something is not right',
user : user
});
}
req.login(user, {session: false}, (err) => {
if (err) {
res.send(err);
}
var token = authenticate.getToken({_id: user.id});
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.json({
success: true,
token: token,
status: 'You are successfully logged in'
})
});
})(req, res);
});
Voici comment on appelle cette url dans postman
Le retour JSON token signifie bien que la connexion est bien réussie.
Si on avait mis un mauvais mot de passe ou identifiant. On aurait le retour suivant :
Nous allons nous servir du token pour récupérer les informations accessibles uniquement en tant qu’utilisateur connecté.
On restreint l’accès à une URL comme ceci
/**
* @GET /api/user/me
*/
router.get('/me',authenticate.verifyUser, function(req, res, next) {
res.json({
error: false,
data: req.user
});
});
Grâce au 2ème argument authenticate.verifyUser que nous avons défini dans /config/middleware/authenticate.js. On pourra restreindre l’accès à l’URL http://localhost:3000/api/user/me
Faisons cela alors
Oups, on se retrouve avec “Unauthorized”. Tout simplement parce qu’à aucun moment dans l’appel, nous avons passé le token donné lors de la connexion.
Ajustons cela dans postman. On va dans la partie “Authorization” puis dans TYPE et on choisit Bearer Token. On colle la valeur du token dans le champ texte à droite.
Cette fois ci c’est bon
SUPER_ADMIN
On va récupérer les informations d’un utilisateur dont on a l’identifiant. Pour cela, il faut avoir le role “SUPER_ADMIN”. Ajoutons la restriction authenticate.verifyAdmin
/**
* @GET /api/user/1
*/
router.get('/:id', authenticate.verifyUser, authenticate.verifyAdmin, function(req, res, next) {
const userId = req.params.id;
db.fos_user.findOne({ where: { id: userId } }).then(api => res.json({
error: false,
data: api
}));
});
Et on pourra facilement récupérer les informations d’un utilisateur comme ceci
Code source: https://github.com/rabehasy/nodejs-express/blob/step3/routes/rest/user.js
CORS
Observons une entête d’une URL sans CORS
exemple: GET http://localhost:3000/api/api
Modifions maintenant /app.js
On ajoute
var cors = require('cors');
Puis
// Cors
app.use(cors());
De cette façon, le cors sera appliqué pour toutes les URLS.
Rafraichissons la même URL et observons l’ajout de
**Access-Control-Allow-Origin →***
Qui prouve la prise en compte du CORS pour cette URL
Si on avait voulu ajouter le cors uniquement pour une URL spécifique, on aurait mis
app.use(cors({
origin: 'https://monsupersite.com'
}));
Ce qui donne
Une application externe à https://monsupersite.com qui essaye d’appeler une URL de notre application se verrait un message d’erreur.
Il est également possible d’ajouter le cors pour une URL spécifique, exemple http://localhost:3000/api/user/me Pour cela, modifions /routes/rest/user.js
Ajoutons
var cors = require('cors');
Puis modifions l’appel en
router.get('/me',authenticate.verifyUser, cors(), function(req, res, next) {
res.json({
error: false,
data: req.user
});
});
Codes sources du projet: https://github.com/rabehasy/nodejs-express/tree/step3
Ce post est publié aussi sur Medium