Bonjour,

J'essaye en ce moment de mettre en place un système d'authentification 'propre'.
J'ai donc décider d'utiliser les librairies jwt et expressJwt du côté de mon serveur (qui est sous Express).

var jwt = require('jsonwebtoken');
var expressJwt = require('express-jwt');

Lorsque j'ai une demande de Login, je regarde si l'utilisateur existe en base et si oui je génère un token que je suis sensé renvoyé au client pour qu'il puisse naviguer normalement par la suite.
Mais il y a une chose qui m'échappe, lorsque je fais un res.json(token) cela me renvoie juste le token, mais comment ensuite avec VueJS je continue sur la page souhaitée et qui est protégée par token ?

Merci pour votre aide,

28 réponses


betaWeb
Réponse acceptée

Salut,

As-tu compris le fonctionnement de OAuth (le protocole d'authentification que tu utilises) ?
En gros,chaque user a une clé secrète que tu génère lors de la création du compte. Puis, lors du login de l'user, tu génères un token via cette clé secrète, puis tu renvoies ce token au client. Un fois que le client l'a récupéré, tu le stockes dans les cookies par ex. puis, pour chaque requète à ton API, tu renvoies dans un header particuler x-access-token le token puis, côté API, tu vérifies que celui-ci correspond bien.

Voici un exemple que j'avais fait pour tester la lib jwt avec Express (ici c'est le router) :

'use strict'

class Router {
    constructor(express, app) {
        this._express = express
        this._app = app

        this.api()
    }

    api() {
        var apiRoutes = this._express.Router()

        /* Cette route sert à authentifier un utilisateur
         * (via des infos qui sont stockées en dur dans un fichier de config dans mon cas vu que c'était pour un test)
         */
        apiRoutes.get('/authenticate', (req, res) => {
            let info = {
                name: global.cfg.app.name,
                id: global.cfg.app.id
            },
            secret = this._app.get('az79Plkd'),
            token = global.jwt.sign(info, secret)

            return res.json({ token })  
        })

        apiRoutes.use((req, res, next) => {
            // On récupère le token envoyé par le front
            let token = (req.body && req.body.token) || (req.query && req.query.token) || (req.headers && req.headers['x-access-token'])

            if (token) {
                // On vérifie que le token correspond bien
                jwt.verify(token, this._app.get('az79Plkd'), (err, decoded) => {
                    if (err) {
                        return res.json({
                            success: false,
                            message: 'Failed to authenticate token.'
                        })
                    } else {
                        req.decoded = decoded

                        next()
                    }
                })
            } else {
                return res.status(403).send({
                    success: false, 
                    message: 'No token provided.'
                })
            }
        })

        apiRoutes.get('/users', (req, res) => res.json({ message: 'Welcome to ' + global.cfg.app.name }))

        this._app.use('/api', apiRoutes)
    }
}

module.exports = Router
Virax
Réponse acceptée

Salut,

Alors pour répondre à ta question de façon rapide, tu peux toujours utiliser les sessions côté navigateur, grace à la variable globale "sessionStorage"
Ici la doc: https://developer.mozilla.org/fr/docs/Web/API/Window/sessionStorage
(Fonctionne même sous ie8 lol)

Bref, du coup ce que je te conseille de faire, c'est de checker lorsque l'utilisateur recharge la page, s'il y a un token dans le sessionStorage, tu utilises celui ci dans ton app vueJs (à toi de voir si tu le socke dans un store avec vueX, ou simplement un objet global à ton appli).

Dans le cas contraire s'il n'a pas de token, tu peux le rediriger vers la page login ou autre...

Voici un example de code que j'utilise avec VueJS 2:
(J'utilise un systeme de routes white-listés, toutes les autres seront redirigées vers la page login);
Ensuite, à la fin, juste avant de monter l'application globale, je fais un petit checkAuth(), qui va regarder si oui ou non j'ai un token dans mon sessionStorage.

Ensuite, juste avant de faire tes appels apis, tu peux récupérer ce token et l'envoyer avec tes requetes, dans mon cas par exemple, j'utilise la libraire axios, et j'assigne mon token de façon globale à toutes mes requêtes de la manière suivante :
(auth.getToken() récupère just le token en session)

 axios.defaults.headers.common['Authorization'] = auth.getToken();

Ps: si tu veux des sessions persistentes, tu peux toujours utiliser le localStorage, qui lui ne sera pas effacé lorsque l'utilisateur ferme son navigateur, oui le sessionStorage est effacé à la fermeture, à toi de voir donc).

Si jamais par sécurité, tu veux annuler un token, tu peux regarder du côté de la librairie: (A faire au moment du logout donc)
https://www.npmjs.com/package/express-jwt-blacklist

Hésites pas si tu as des questions sur le code !

var whitelist = [
    '/activation',
    '/login',
    '/register'
];

router.beforeEach( (to, from, next) =>
{
    if(to.path.match(/admin/) && auth.user.profile.role != 'admin')
    {
        next({path: '/'});
        return
    }

    if(to.path.match(/activation/))
        next();
    else {
        if(whitelist.indexOf(to.path) == -1 && !auth.isAuth())
        {
            next({
                path: '/login',
                query: {redirect: to.fullPath}
            });
        }
        else
            next()
    }
});

auth.checkAuth( _=>
{
    new Vue(Vue.util.extend(
        {
            router,
            store
        },
        require('./App.vue'))
    )
    .$mount('#app');
});
Neewd
Auteur

Ouais j'ai bien compris l'utilisation du token, mais moi ce que je ne comprends pas, c'est côté front, tu dis que tu le stockes dans une session, mais dans VueJS tu fais comment ça ? Par une librairie ?
Car lorsque je fais un res.json({token}), le token s'affiche juste dans ma page.
Il y a quelque chose que je ne pige pas ^^

Fais-voir ton code ?
Et il te faut créer un Store pour le user connecté, dans lequel tu stockes le token et voilà ;)

Neewd
Auteur

C'est vrai que j'avais pas penser au store pour stocker le token.
En fait, avant j'utilisais VueResource pour balancer un this.http.post('/login') qui comprenait ma logique d'authentification et je renvoyais true ou false.
Et étant donné que je n'arrivais pas à ensuite aller sur une autre page par un redirect ou je ne sais quoi, je suis passé par la méthode classique du action="/login" method="post" de mon formulaire.

Je vais essayer de repasser par VueResource et faire un this.$route.go (je ne sais pas si ça peut exister :p)

Merci en tout cas pour tes réponses !

Neewd
Auteur

Salut Virax, et merci pour ta réponse,

J'ai un peu avancé, et là j'en suis à la création de mon store pour stocker le token.
Je vais jeter un oeil du côté d'axios pour assigner - comme tu le fais - automatiquement le token au HEADER.

J'ai juste une question sur la syntaxe de ce bout de code

auth.checkAuth( _=>
{
    new Vue(Vue.util.extend(
        {
            router,
            store
        },
        require('./App.vue'))
    )
    .$mount('#app');
});

Comme je ne suis pas encore super familier avec la syntaxe ES6 de ce que je comprends c'est qu'au moment de créer ton instance VueJS et de l'attacher à #app, tu appelles auth.checkAuth() ?
Et à quoi sert le "_" ?

Rien de bien méchant c'est juste le nom de la fonction anonyme du callback,
tu peux le voir également de cette façon :

auth.checkAuth(function() {

Au passage en cadeau, ma fonction checkAuth()
(Je fais également une requête à mon api, qui me renvoie un booléen, si la session du côté serveur (express), est vide ou non !)

checkAuth(cb)
    {
        $.getJSON(LOGIN_URL, {}, (res) =>
        {
            if(res.success)
            {
                this.user.profile = JSON.parse(localStorage.getItem('session_profile'));
                axios.defaults.headers.common['Authorization'] = auth.getToken();
                this.user.authenticated = true;
            }
            else
                auth.clearSession();

            if(typeof cb == 'function')
                cb()
        });
    },

@Neewd Les fonctions fléchées (oui ça s'appelle comme ça ^^) sont apparues avec l'ES2015 (donc assez récemment). A savoir qu'elles ont la particulatiré d'hériter du contexte parent (l'appel à this au sein d'une fonction fléchée fera référence au this parent).
Si tu veux plus d'info, va voir ici https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Fonctions/Fonctions_fl%C3%A9ch%C3%A9es

Neewd
Auteur

Ouais j'avais regardé un article sur les nouvelles fonctionnalités de l'ES6 mais tant que l'on ne pratique pas on ne peut pas se souvenir de tout.
J'essaye petit à petit d'utiliser cette syntaxe mais parfois ça ne veut pas rentrer :p

Je vais voir pour implémenter Axios avant l'envoi de mes requêtes.
Par contre j'ai une petite question.
Une fois que mon token est enregistré (peu importe où) si j'utilise VueRouter je n'enverrais aucune "requête" puisque tout sera fait par Vue ?
Donc comment checker que mon client est bien logguer ?

Bien sûr que si tu vas envoyer des requêtes, sinon comment vas-tu récupérer tes resources ?
Le routing et l'AJAX sont deux choses complètement différentes.
Es-tu sûr d'être à l'aise avec tous ces concepts ?

Neewd
Auteur

Lorsque tu instancies ton router dans VueJS tu charges tous tes composants et selon l'URL ça charge les bons composants.
Je sais bien ce qu'est l'AJAX, et le routing aussi, mais ce qui me dérange c'est l'utilisation combiné du routing Vue et du routing server via Express :/

Donc tu n'as pas forcément compris : dans ton cas, ton serveur NodeJS (via ExpressJS) fait office de serveur de ressources et non serveur de routage. En gros c'est ton API sur laquelle tu vas récupérer tes data. Il n'y a rien d'anormal là dedans ne t'inquiètes pas ;)

Neewd
Auteur

Oui mais dans le routing server je comprenais le render des views, on peut effectivement considéré ça comme des ressources.
Mais étant donné que je vais essayer de passer par VueRouter, comment faire pour faire un checkAuth avant de charger un nouveau component avant de .push('path'); ?

Tout dépend de quelle façon tu peux architecturer ton app : soit tu passes par un système de render via le back, soit tu passes par un système de routes sur le front et là tu utilises ton back comme API. Tu peux mélanger les deux bien évidemment. Cela dit, je ne vois toujours pas ce qui te pose souci ? Pour check ton user, il faut ajouter le middleware qui va bien sur les routes concernées (côté ExpressJS).

C'est ce que j'ai fait dans l'exemple que je t'ai donné plus haut :

apiRoutes.use((req, res, next) => {
            // On récupère le token envoyé par le front
            let token = (req.body && req.body.token) || (req.query && req.query.token) || (req.headers && req.headers['x-access-token'])

            if (token) {
                // On vérifie que le token correspond bien
                jwt.verify(token, this._app.get('az79Plkd'), (err, decoded) => {
                    if (err) {
                        return res.json({
                            success: false,
                            message: 'Failed to authenticate token.'
                        })
                    } else {
                        req.decoded = decoded

                        next()
                    }
                })
            } else {
                return res.status(403).send({
                    success: false, 
                    message: 'No token provided.'
                })
            }
        })
Neewd
Auteur

Oui je pense plutôt que je vais partir sur un routage côté client et me servir du serveur uniquement pour l'API ça sera plus "clair".
J'avais déjà essayé cette solution, mais ça me posait problème puisque ce bout de code était appelé systématiquement et non pas uniquement sur les routes sur lesquelles je voulais une auth.

Je pense faire un petit router.beforeEach et checker le store pour voir si l'utilisateur peut y aller.

Ce qui m'a handicapé est le faire de vouloir mélanger le technos sans les connaitre parfaitement donc je suis partis sur des bases bancales et j'étais obligé de revenir en arrière pour revoir certains trucs.
Très très chiant.

Beh il faut déclarer un middleware que tu utilisera uniquement sur les routes que tu veux.
Voici un exemple :

middlewares.js

var middlewares = {
  auth: function (req, res, next) {
            // On récupère le token envoyé par le front
            let token = (req.body && req.body.token) || (req.query && req.query.token) || (req.headers && req.headers['x-access-token'])

            if (token) {
                // On vérifie que le token correspond bien
                jwt.verify(token, this._app.get('az79Plkd'), (err, decoded) => {
                    if (err) {
                        return res.json({
                            success: false,
                            message: 'Failed to authenticate token.'
                        })
                    } else {
                        req.decoded = decoded

                        next()
                    }
                })
            } else {
                return res.status(403).send({
                    success: false, 
                    message: 'No token provided.'
                })
            }
}

module.exports = middlewares;

app.js

let mw = require('./middlewares') 

/* init de ton app ici */

/* Exemple de route avec le middleware authentification */
app.get('/user/:id', mw.auth, (req, res) => {
  /* ton code ici */
});
Neewd
Auteur

Ah j'avais pas pensé à faire ça comme ça.
Moi j'avais juste fait un router.use(function() { code de decode de token }) et donc à CHAQUE route c'était appelé et ma page de login était à juste titre non accessible ^^

Il y a plusieurs façons de faire avec ExpressJs : sois tu déclares tes routes une par une et tu leur applique le(s) middleware(s) que tu souhaites, comme dans l'exempel précédent, soit tu peux définir un ou plusieurs groupes de routes (comme sur le premier exemple que j'ai posté), auquel cas tu peux définir des middlewares spécifique à ces groupes de route (via la méthode app.use()).

Neewd
Auteur

C'est vrai que là comme je suis sur un petit projet personnel j'étais parti sur une déclaration unitaire.
Mais en fait c'est surtout que j'avais pas pensé à l'empilage des middlewares pour une route.

En tout cas merci beaucoup pour tous vos messages à toi et Virax pour votre temps et votre savoir ça fait plaisir d'apprendre dans ces conditions là :D

Pas de quoi, on est là pour ça ;)

Ca peut être un poil complexe quand on a jamais trop touché à un système d'OAuth (authentification via token).
D'ailleurs, je te conseille de lire ceci : http://www.bubblecode.net/fr/2016/01/22/comprendre-oauth2/

Neewd
Auteur

Effectivement je n'avais jamais toucher au système d'OAuth, alors je connaissais les bases, je savais ce qu'était un token mais je ne l'avais jamais pratiqué donc c'était surtout sur l'implémentation que sur la théorie :)

Neewd
Auteur

Hey,

Je reviens vers vous car j'ai un petit problème avec mon store et c'est vraiment très spécial.

Voici mon store.

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

const state = {
    user: {
        token: '',
        username: ''
    }
}

const mutations = {
    MODIFY_USER: (state, token, username ) => {
        state.user.token = token;
        state.user.username = username;
    }
}

const getters = {
    username: (state) => state.user.username,
    token: (state) => state.user.token
}

const actions = {
    modifyUser: (store, token, username) => {
        store.commit('MODIFY_USER', {
            token : token,
            username : username
        });
    }
}

let store = new Vuex.Store({
    state: state,
    mutations: mutations,
    getters: getters,
    actions: actions
})

export default store

Et lorsque je demande la modification du user et que je met le token à jour, toutes les données sont créer dans un objet token c'est très bizarre.
Voici la description dans Vuex via Chrome.

Quelqu'un aurait une idée ?

Oui une mutation ne peut accepter que deux paramètres : le store, et un seul autre argument.
Si tu veux passer plusieurs data en paramètre, ça donnerait un truc comme ça:

Ps: Idem pour les actions d'ailleurs, seulement deux paramètre, le store et les "data"
Ps2: tu n'est pas non plus obligé de déclarer toutes les sous propriétées de ton "user" dans le state, seulement déclarer user: {} suffit :)

   # Pour la mutation
   MODIFY_USER: (state, data) => {
        state.user.token = data.token
        state.user.username = data.username
    }

   # ou alors directement passer le user
    MODIFY_USER: (state,  data) => {
        state.user = data
    }

    # Et pour l'action
     modifyUser: (store,  user) => {
        store.commit('MODIFY_USER',  user);
    }
Neewd
Auteur

Ah effectivement j'ai essayé ce matin et ça marche parfaitement bien.

Maintenant j'ai un nouveau problème. Quand j'accède à mon store depuis mon router.beforeEach() il me semble que le state n'est pas encore modifié quand le router.beforeEach() est appelé, donc mon store.user reste à undefined.

Est-ce qu'on pourrait voir le code de ton beforeEach, ça serait plus simple !

Neewd
Auteur

En fait, hier j'ai fais quelques tests, quand je passe par le getters, je tombe sur un undefined, par contre quand je fais directement store.state.user.username (ou token) j'ai bien une valeur.
Très étrange, j'ai pas pu pousser plus loin :/

comment utilises-tu tes getters ? via un mapGetters sur la propriété "computed" de ta vue c'est bien ça ?

Es-tu sûr d'avoir apeller la methode qui "set" le utilisateur, en l'occurence modifyUser, avant d'utiliser ton getter ?

Tu peux utiliser la fonction "created" de ta vue, ou alors directement utiliser le mapActions sur la propriété "methods",
que tu peux ensuite récupérer via une action "click" par exemple !

Neewd
Auteur

Désolé de répondre que maintenant, avec les fêtes et après les vacances je ne reviens que maintenant :)

En gros j'ai un tout petit peu avancé et j'utilise simplement le store avec store.state.mavariable en lieu et place de store.state.getters et ça fonctionne correctement.

Je ne sais pas si c'est la bonne façon de faire.