Imprimer
Catégorie : R4A.10 - Compléments web (vuejs)
Affichages : 2804

Préambule

 

Truc utile :

VUE_APP_API_ENDPOINT=https://localhost:3334/authapi

 


1°/ Authentification

A l'heure actuelle, de nombreuses applications web ne gèrent plus elles-mêmes l'authentification de leur utilisateurs. Elles font appel à des web services que proposent bon nombres d'entreprises, notamment google, amazon, microsoft, etc. Ce mécanisme n'est cependant pas applicable lorsque les accès aux parties privées nécessitent des informations qui ne sont pas disponibles dans un compte de type google/microsoft. C'est souvent le cas avec les applications internes à une entreprise. Dans ce cas, il faut utiliser les bases de données utilisateurs de l'entreprise. NB: ces BdD peuvent malgré tout être externalisées dans un cloud et accessibles via des web services, mais est-ce judicieux ?

Pour cela, soit l'application web fait elle-même l'authentification en accédant directement à la base données, soit elle fait appel à un serveur d'authentification interne, par exemple un serveur CAS (comme à l'université). Comme on est dans le cadre d'une application web, s'exécutant au sein d'un navigateur, l'authentification se fait à priori sur la base de requête http. C'est pourquoi les principes d'authentification restent généralement les mêmes :

Historiquement, le contenu de ces informations a été très divers, même si le principe restait le même : utiliser une ou plusieurs valeurs créés par le serveur, que le navigateur va renvoyer à chaque requête. Le serveur peut ainsi vérifier si la requête doit être bloquée car le navigateur n'envoie pas des valeurs valides. L'un des premiers type de valeur utilisé dans les sites en php a été un identifiant de session, souvent sous la forme d'une chaîne de caractères de type uuid. Cet identifiant était soit renvoyé "en clair" dans l'URL des requêtes suivantes, soit dans le corps de la requête, soit sous forme de cookie. A l'heure actuelle, on utilise plutôt les Json Web Token, dont le contenu est crypté donc non lisible directement côté navigateur.

En revanche, la façon dont le serveur renvoie renvoie les informations a pris essentiellement 2 formes :

L'avantage des cookies est qu'ils sont automatiquement renvoyés par le navigateur lors des requêtes suivantes au serveur. Cependant, il faut prendre garde à la sécuriser si l'on ne veut pas qu'ils soient accessibles et piratables, via du code javascript. Si on reçoit du xml/json, il faut stocker les informations en mémoire, ce qui signifie qu'elles sont forcément accessibles via du javascript. Il faut également penser à renvoyer ces informations lors de chacune des requêtes suivantes. En apparence, la solution des cookies semble plus simple et efficace mais on va voir que la meilleure solution consiste a mixer les 2 moyens.

 

2°/ Les JSON Wen Token (JWT)

2.1°/ principe

Un JWT est simplement un objet JSON contenant une partie donnée et une partie indiquant la validité de cette donnée. La donnée est cryptée grâce à une clé symétrique qui n'est connue que par l'émetteur du JWT, donc généralement le serveur.

Les données stockées dans le JWT peuvent être aussi diverses qu'un identifiant de session, un nom d'utilisateur, des informations liées à l'accès à l'application web, etc. C'est le serveur qui décide de ce qu'il y a à l'intérieur et le navigateur ne peut en principe pas décrypter ses informations. Si le JWT est modifié d'une quelconque façon, par exemple par un pirate qui aurait réussi à se le procurer, les informations stockées dedans restent normalement inaccessibles et toute tentative de modification du JWT provoquera une erreur au niveau du serveur qui, à priori, n'arrivera pas à le décrypter correctement.

2.2°/ failles classiques

En revanche, le simple fait de récupérer le JWT peut donner au pirate l'occasion d'usurper l’identité du propriétaire légitime, selon la façon dont le serveur est implémenté. En effet, s'il suffit d'envoyer un JWT en cours de validité pour accéder aux pages privées d'un site, alors en voler un autorise un piratage facile.

2.2.1°/ faille XSS

Une des façons les plus courantes de récupérer un JWT est d'explorer l'un des espaces de stockage locaux du navigateur. Par exemple, l'espace local représenté par la variable localStorage en javascript, permet de stocker des clés, associées à des valeurs. On peut donc stocker un JWT en l'associant à un nom.

Malheureusement, n'importe quel code javascript s'exécutant au sein du navigateur permet d'accéder au localStorage et donc récupérer les données liées à une clé, pourvu que l'on connaisse le nom de la clé. Pour trouver ce dernier, il suffit d'enregistrer le code javascript de l'application web et de faire une recherche sur localStorage.

Pour exécuter un code JS accédant au localStorage, il suffit de faire un peu de "social engineering" et persuader un utilisateur de l'application de cliquer sur un lien vers un site, ce dernier envoyant au navigateur une page avec du JS accédant au localStorage, pour l'envoyer au pirate, donc avec le JWT dedans. Le pirate peut donc se faire passer pour l'utilisateur. Ce type de faille est nommée XSS = Cross Site Scripting car un utilise un site fournissant un script JS permettant d'attaquer un autre site écrit en JS.

Il est également possible que l'application contienne une faille permettant au pirate d'enregistrer des données contenant du JS, dans les Bdd utilisées par le site. Quand un utilisateur demande une page qui affiche ces données, le navigateur exécutera le code JS, qui accédera au localStorage pour envoyer le JWT de l'utilisateur. Ce type de faille utilise le principe de " l'injection de code" puis le XSS.

2.2.2°/ faille XSRF

XSRF = Cross Site Request Forgery.

Cette faille est utilisable lorsque le JWT est stocké dans un cookie, au lieu du localStorage. La première précaution à prendre est de rendre les cookies non accessibles via du JS. Cela se fait dans le code du serveur qui envoie le cookie (cf. attribut httpOnly). Si cette précaution n'est pas prise, alors un pirate peut utiliser directement une faille de type XSS pour récupérer le cookie.

Sinon, il faut également que le navigateur accepte de stocker ce cookie. En effet, si un navigateur visite un site A, qui fait appel à une API B avec un nom de domaine différent, et que B envoie un cookie au navigateur, ce dernier refuse par défaut le cookie. Il faut que B configure l'attribut sameSite du cookie afin de forcer la main au navigateur. Ensuite, chaque fois que le navigateur fera une requête HTTP vers B, le cookie sera automatiquement renvoyé.

C'est justement ce renvoi automatique (nécessaire si l'API utilise un cookie JWT pour authentifier les requêtes) qui peut être exploité par un pirate :

2.3°/ principes pour limiter les XSS + XSRF

Il n'existe malheureusement pas de méthode infaillible mais on peut remarquer que les deux solutions (localStorage et cookie) ne sont pas sensibles au même type de faille. Une solution à peu près sécurisée consiste donc à mixer les deux principes :

Quand le navigateur envoie une requête au serveur :

Quand le serveur reçoit une requête :

 

ATTENTION : Il faut bien comprendre que cette méthode n'est pas infaillible puisqu'il est toujours possible via une faille XSS de récupérer le token xsrf de l'utilisateur, puis d'utiliser une faille XSRF. Cependant, cela limite fortement la probabilité qu'une telle attaque réussisse.

 

2.4°/ Rafraîchir un JWT : les pours et les contres

Pour limiter les possibilités d'attaque, on lit fréquemment qu'il convient surtout de limiter le temps de validité d'un JWT. C'est très facile quand le JWT est dans un cookie puisque la durée de vie de ces derniers est fixée par le serveur qui envoie le cookie. Par exemple, si un JWT est valide pendant seulement une minute, un pirate n'a quasi aucune chance de récupérer le token xsrf via une faille XSS pour ensuite lancer une attaque type XSRF. 

Cependant, cela impliquerait de se reloguer toutes les minutes, ce qui est impensable. De ce fait, on doit mettre en place un mécanisme permettant de "rafraîchir" le JWT :

A noter que le token de rafraîchissement a également une validité, généralement assez longue (plusieurs minutes/heures) par rapport à celle du JWT.

Malheureusement, si les token xsrf et rafraîchissement sont stockés dans le localStorage, une faille XSS permet à un pirate d'accéder au contenu du localStorage. Il peut donc demander lui-même un rafraîchissement et obtenir un JWT valide. On aboutit donc à une situation presque pire puisque le pirate peut se faire passer pour l'utilisateur, comme avec une faille XSS. Même si on stocke le token de rafraîchissement dans le store, un pirate chevronné analysera le code JS de l'application et réussira à accéder à la variable contenant le token.

Heureusement, il existe des solutions pour compliquer la tâche aux pirates. Par exemple :

 

2.5°/ Encore plus de sécurité ?

Enfin, il est également possible d'ajouter d'autres précautions, plus exotiques :

 

3°/ mise en place avec vuejs.

On suppose que le serveur est une API de type REST, avec une route /login. La méthode POST est utilisée pour envoyé un objet json contenant le login et le mot de passe fournis par l'utilisateur.

3.1°/ contraintes liées à l'utilisation des API

Les navigateurs modernes sont très restrictifs avec les requêtes asynchrones. En effet, quand les pages actuellement consultées dans le navigateur proviennent d'un site A, et qu'une de ces pages fait une requête asynchrone vers un site B, le navigateur peut refuser d'en charger le contenu. C'est le mécanisme CORS (Cross Origin Resource Sharing) intégré au navigateur qui va autoriser ou non le contenu en fonction des entêtes fournies par B. Attention : une simple différence de n° de port est significative pour CORS. Par exemple, quand on test en local un site A fourni par localhost:8080 et que l'API REST est accessible par localhost:4567, alors CORS considère que ce sont 2 origines différentes.

Par défaut, si le site B ne fait rien pour envoyer les bonnes entêtes dans les réponses aux requêtes du client, le navigateur bloquera le contenu, y compris les cookies qui ne seront pas sauvegardés dans la mémoire du navigateur.

Par exemple, dans un serveur écrit en node + express, on peut utiliser le module cors afin de positionner correctement les entêtes :

const express = require('express');
const cors = require("cors");

const app = express();
var corsOptions = {
  origin: [ /.*$/ ], // ATTENTION : toute origine est acceptée = trou de sécurité potentiel
  credentials: true,
  allowedHeaders: "x-xsrf-token, Origin, Content-Type, Accept",
};
app.use(cors(corsOptions));
...

 

Explications :

 

3.2°/ l'application SPA

Le code de démonstration n'utilise pas toutes les astuces destinées à compliquer la tâche à un pirate. Pour résumer, il utilise :

 

3.2.1°/ configuration de vue

Afin de faciliter le passage d'une version de développement à une version de production, il est utile de définir deux fichiers donnant l'URL de l'API, en mode dev. ou bien prod. Ces 2 fichiers sont à mettre à la racine du projet. Exemple :

fichier .env.development

VUE_APP_API_ENDPOINT=https://localhost:4567/authapi

 

fichier .env.production

VUE_APP_API_ENDPOINT=https://myapi.domain.fr:443/authapi

 

La variable VUE_APP_API_ENDPOINT peut ensuite être utilisée dans le reste du code, grâce à process.env.

Pour simplifier encore, il est possible de créer un fichier général de configuration de l'application que l'on importe dans la plupart des autres fichiers. Par exemple :

fichier config.js

export default {
  urlAPI: process.env.VUE_APP_API_ENDPOINT
}

 

On peut ensuite utiliser cette variable comme base pour les requêtes d'axios. Par exemple, dans le fichier axios.service.js :

import axios from 'axios'
import Config from '../commons/config'
...
// create a special axiosAgent agent that works with the apidemo API
const axiosAgent = axios.create({
    baseURL: Config.urlAPI,
    withCredentials : true,
});

 

3.2.2°/ les services

Le plus simple et fonctionnel consiste à créer différents fichiers représentant les "services" fonctionnels. Par exemple :

Comme dit précédemment, le jwt étant stocké dans un cookie, il sera automatiquement envoyé à chaque requête, pourvu que le navigateur soit configuré pour cela. Par défaut, les cookies ne seront pas envoyés à des sites d'une autre origine. Lors de l'envoi d'un requête il faut donc dire explicitement de les envoyer. On peut configurer l'agent axios pour que ce soit fait automatiquement. Par exemple, dans axios.service.js, on écrit :

import axios from 'axios'
import https from 'https'  // pour faire des requêtes en https.

const myaxios = axios.create({
    httpsAgent: new https.Agent({rejectUnauthorized: false}),
    withCredentials: true, // envoi auto. des cookies à d'autres origines
});

 

Quant au token xsrf, comme il est possible de créer artificiellement des entêtes aux requêtes HTTP, il suffit d'en ajouter une dont la valeur est le token xsrf, et d'envoyer cette entête à chaque requête axios.

Pour cela, le plus simple est d'utiliser les "intercepteurs" axios. Ils permettent de définir des fonctions qui seront appelées automatiquement dans certaines situations, notamment quand on veut envoyer une requête. Par exemple :

 

myaxios.interceptors.request.use(
    config => {
        return {
            ...config,
            headers: {
                ...AuthService.authHeader()
            },
        };
    },
    error => Promise.reject(error)
);

 La méthode authHeader() renvoie simplement un objet du style { x-xsrf-token : "..." }, où la valeur provient du localStorage, si elle existe. Sinon, on ne renvoie rien.

Grâce à cet intercepteur, on ajoute l'entête x-xsrf-token à toutes les requêtes vers le serveur, à partir du moment où l'authentification a été réussie et que l'on a reçu une valeur pour le token xsrf.

 

3.2.3°/ le store

Le store vuex va permettre de conserver les informations de connexion, notamment les token xsrf, et si l'utilisateur est logué ou non. Cette information est généralement essentielle pour faire de l'affichage conditionnel de contenu.

Pour cela, il convient de créer un store modulaire, avec un module pour gérer l'authentification, et d'autres pour gérer les données applicatives. Pour l'authentification, on peut créer un fichier auth.js, avec :

3.2.4°/ Le router

Les routes du front-end sont définies comme dans le TD 1, avec notamment un mécanisme qui empêche de suivre des routes qui nécessitent une authentification réussie.

 

3.2.5°/ Le composant d'enregistrement

Ce composant fournit une interface permettant de créer un nouvel utilisateur. Il sert uniquement à montrer comment utiliser l'API recaptcha fournie par google, au travers d'un module npm nommé vue-recaptcha.

Pour résumer, l'utilisation de ce module nécessite de créer une paire de clé sur le site de google qui gère recaptcha : https://www.google.com/recaptcha/

Avec un compte google, il est possible de créer cette paire de clé. L'une de ces clés doit être utilisée dans le front-end et l'autre dans le back-end. Il n'y a effectivement pas de possibilité de faire avec uniquement le front. En pratique, quand on valide le captcha, il faut ensuite envoyer au back-end le résultat de cette validation. Ce dernier fait ensuite appel à l'API google recaptcha pour vérifier si la validation est correcte ou non.

Dans le cas présent, le formulaire d'enregistrement fait appel à une route du back-end, en envoyant un login, mot de passe et nom de héro, plus de résultat de la validation du captcha. Ensuite, le back-end interroge l'API google et si tout est bon, il peut ensuite vérifier les informations fournies avant de créer le nouvel utilisateur.

 

3.3°/ Démonstration

NB : l'API est réglée pour que le JWT expire au bout de 30 secondes.

Remarque :

 

 

 4°/ Bonus : installer le back-end.

Le back-end repose sur une BdD mongodb v5. Il faut donc installer ce sgbd en premier lieu.