Expressjs — cors et jwt


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

img

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

img

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 :

img

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

img

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

img

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

img

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

img

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

img

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

img

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
    });
});

img

Codes sources du projet: https://github.com/rabehasy/nodejs-express/tree/step3

Ce post est publié aussi sur Medium