Comment sécuriser les applications React contre les attaques XSS avec des cookies HTTP uniquement

De Get Docs
Aller à :navigation, rechercher

Introduction

L'authentification basée sur les jetons peut sécuriser les applications Web qui associent des ressources publiques et privées. L'accès aux actifs privés nécessite qu'un utilisateur s'authentifie avec succès, généralement en fournissant un nom d'utilisateur et un mot de passe secret que seul l'utilisateur connaît. Une authentification réussie renvoie un jeton pour la durée pendant laquelle l'utilisateur décide de rester authentifié, de sorte que l'utilisateur peut fournir le jeton au lieu d'avoir à s'authentifier à nouveau à chaque accès aux actifs privilégiés. L'utilisation des jetons soulève la question essentielle de savoir où stocker les jetons pour les protéger. Les jetons peuvent être stockés dans le stockage du navigateur en utilisant les propriétés Window.localStorage ou Window.sessionStorage, mais cette méthode est vulnérable aux attaques cross-site scripting (XSS) car le contenu du stockage local et de session est accessible à tout JavaScript exécuté sur le même document qui stocke les données.

Dans ce didacticiel, vous allez créer une application React et une API fictive qui implémentent un système d'authentification basé sur des jetons configuré dans un conteneur Docker local pour des tests cohérents sur toutes les plates-formes. Vous commencerez par implémenter l'authentification basée sur les jetons en utilisant le stockage du navigateur avec le Window.localStorage propriété. Ensuite, vous exploiterez cette configuration avec une attaque de script intersite réfléchie pour comprendre les vulnérabilités de sécurité présentes lors de l'utilisation du stockage du navigateur pour conserver des informations secrètes. Vous améliorerez ensuite cette application en la transformant en cookies HTTP uniquement stockant le jeton d'authentification, qui ne sera plus accessible au code JavaScript potentiellement malveillant qui pourrait être présent sur le document.

À la fin de ce didacticiel, vous comprendrez les considérations de sécurité nécessaires pour implémenter un système d'authentification basé sur des jetons fonctionnel parallèlement à une application Web React et Node. Le code de ce didacticiel est disponible dans le DO Community GitHub.

Conditions préalables

Pour terminer ce tutoriel, vous aurez besoin des éléments suivants :

Étape 1 - Préparation d'un conteneur Docker pour le développement

Dans cette étape, vous allez configurer un conteneur Docker à des fins de développement. Vous commencerez par créer un Dockerfile avec des instructions pour créer une image afin de créer votre conteneur.

Créez et ouvrez un fichier appelé Dockerfile à l'intérieur de votre répertoire personnel en utilisant nano ou votre éditeur préféré :

nano Dockerfile

Placez-y les lignes de code suivantes :

Fichier Docker

FROM node:18.7.0-bullseye

RUN apt update -y \
    && apt upgrade -y \
    && apt install -y vim nano \
    && mkdir /app

WORKDIR /app

CMD [ "tail", "-f", "/dev/null" ]

La FROM crée la base de votre image à l'aide de la ligne prédéfinie node:18.7.0-bullseye de Dockerhub. Cette image est construite avec les dépendances NodeJS nécessaires installées, ce qui rationalisera votre processus de configuration.

La RUN met à jour et met à niveau les packages et cette ligne installe également d'autres packages dont vous pourriez avoir besoin. La WORKDIR ligne définit le répertoire de travail.

La CMD définit le processus principal à exécuter à l'intérieur du conteneur, garantissant que le conteneur continuera à fonctionner afin que vous puissiez vous y connecter et l'utiliser pour le développement.

Enregistrez et fermez le fichier.

Créez l'image Docker avec le docker build commande, remplacement path_to_your_dockerfile avec le chemin vers votre Dockerfile :

docker build -f /path_to_your_dockerfile --tag jwt-tutorial-image .

Le chemin d'accès à votre Dockerfile sera transmis au -f option pour indiquer le chemin du fichier à partir duquel vous allez créer une image. Vous étiquetez cette version en utilisant le --tag option, qui vous permet de vous y référer ultérieurement avec un nom convivial (dans ce cas, jwt-tutorial-image).

Après avoir exécuté le build commande, vous verrez une sortie similaire à ceci :

Output...
 => => writing image sha256:1cf8f3253e430cba962a1d205d5c919eb61ad106e2933e33644e0bc4e2cdc433                                    0.0s 
 => => naming to docker.io/library/jwt-tutorial-image   

Exécutez l'image en tant que conteneur avec la commande suivante :

docker run -d -p 3000:3000 -p 8080:8080 --name jwt-tutorial-container jwt-tutorial-image

La -d flag exécute le conteneur en mode détaché afin que vous puissiez vous y connecter avec une session de terminal distincte.

Remarque : Si vous préférez développer en utilisant le même terminal que celui que vous utilisez pour exécuter le conteneur Docker, remplacez le -d drapeau avec -it, qui vous fournira immédiatement une borne interactive fonctionnant dans le conteneur.


La -p flag redirigera les ports 3000 et 8080 de votre conteneur. Ces ports desservent respectivement les applications frontales et dorsales de votre ordinateur hôte. localhost réseau afin que vous puissiez tester votre application à l'aide de votre navigateur local.

Remarque : Si votre machine hôte utilise actuellement des ports 3000 et 8080, vous devrez arrêter les applications utilisant ces ports, sinon Docker générera une erreur lors de la tentative de transfert des ports.

Vous pouvez également utiliser le -P drapeau pour rediriger les ports de vos conteneurs vers les ports inutilisés de votre machine localhost réseau. Si vous utilisez le -P flag au lieu de mapper des ports spécifiques, vous devrez exécuter docker network inspect your_container_name pour savoir quels ports de conteneurs de développement sont mappés sur quels ports locaux.


Vous pouvez également vous connecter avec VSCode à l'aide du plug-in Remote Containers.

Dans une session de terminal distincte, exécutez cette commande pour vous connecter au conteneur :

docker exec -it jwt-tutorial-container /bin/bash

Vous verrez une connexion comme celle-ci avec l'étiquette de votre conteneur pour indiquer que vous vous êtes connecté :

[email protected]:/app# 

Dans cette étape, vous configurez une image Docker prédéfinie et vous vous connectez au conteneur que vous utiliserez pour le développement. Ensuite, vous configurerez le squelette de votre application dans le conteneur à l'aide de create-react-app.

Étape 2 - Configuration des bases de votre application frontale

Dans cette étape, vous allez initialiser votre application React et configurer la gestion des applications avec un ecosystem.config.js dossier.

Après vous être connecté au conteneur, créez un répertoire pour votre application avec le mkdir commande, puis déplacez-vous dans le répertoire nouvellement créé à l'aide de la cd commande:

mkdir /app/jwt-storage-tutorial
cd /app/jwt-storage-tutorial

Exécutez ensuite le create-react-app binaire à l'aide de la commande npx pour initialiser un nouveau projet React qui servira d'interface à votre application Web :

npx create-react-app front-end

La create-react-app binaire initialise une application React bare-bones avec un README fichier pour développer et tester l'application, ainsi que plusieurs dépendances largement utilisées, y compris react-scripts, react-dom, et jest.

Taper y lorsque vous êtes invité à poursuivre l'installation.

Vous verrez cette sortie de l'appel à create-react-app:

Output...

Success! Created front-end at /home/nodejs/jwt-storage-tutorial/front-end
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd front-end
  yarn start

Happy hacking!

Votre sortie peut varier légèrement avec différentes versions de create-react-app.

Vous êtes prêt à lancer une instance de développement et à commencer à travailler sur votre nouvelle application React.

Pour exécuter l'application, vous utiliserez le gestionnaire de processus PM2. Installer pm2 avec cette commande :

npm install pm2 -g

La -g flag installe le package globalement. Selon les autorisations de l'utilisateur sous lequel vous êtes connecté, vous devrez peut-être utiliser le sudo commande pour installer les packages globalement.

PM2 offre plusieurs avantages lors des étapes de développement et de production d'une application. Par exemple, PM2 vous aide à maintenir les différents composants de votre application en arrière-plan pendant le développement. Vous pouvez également utiliser PM2 pour des besoins opérationnels en production, tels que la mise en œuvre de modèles de déploiement pour corriger votre application de production avec un temps d'arrêt minimal. Pour en savoir plus, vous pouvez lire PM2 : Applications Nodejs prêtes pour la production en quelques minutes.

Le résultat de l'installation ressemblera à ce qui suit :

Outputadded 183 packages, and audited 184 packages in 2m
12 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities -->

Pour exécuter votre application à l'aide du gestionnaire de processus PM2, accédez au répertoire de votre projet React et créez un fichier nommé ecosystem.config.js utilisant nano ou votre éditeur préféré :

cd front-end
nano ecosystem.config.js

La ecosystem.config.js contiendra des configurations pour le gestionnaire de processus PM2 sur la façon d'exécuter votre application.

Ajoutez le code suivant dans le nouveau ecosystem.config.js dossier:

jwt-storage-tutorial/front-end/ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'front-end',
      cwd: '/app/jwt-storage-tutorial/front-end',
      script: 'npm',
      args: 'run start',
      env: {
        PORT: 3000
      },
    },
  ],
};

Ici, vous définissez une nouvelle configuration d'application avec le gestionnaire de processus PM2. La name Le paramètre de configuration vous permet de choisir un nom pour votre processus dans la table de processus PM2 pour une identification facile. La cwd Le paramètre définit le répertoire racine du projet que vous allez exécuter. La script et args Les paramètres vous permettent de sélectionner l'outil de ligne de commande pour exécuter votre programme. Finalement, le env Le paramètre vous permet de transmettre un objet JSON pour définir les variables d'environnement nécessaires à votre application. Vous ne définissez qu'une seule variable d'environnement, PORT, qui définit le port sur lequel l'application frontale s'exécutera.

Enregistrez et quittez le fichier.

Utilisez cette commande pour vérifier quels processus le gestionnaire PM2 exécute actuellement :

pm2 list

Dans ce cas, vous n'exécutez actuellement aucun processus sur PM2, vous obtenez donc cette sortie :

Output┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

Si vous exécutez des commandes et que vous devez réinitialiser le gestionnaire de processus pour une nouvelle ardoise, exécutez cette commande :

pm2 delete all

Maintenant, démarrez votre application en utilisant le gestionnaire de processus PM2 avec les configurations spécifiées dans votre ecosystem.config.js dossier:

pm2 start ecosystem.config.js

Vous verrez une sortie similaire à celle-ci sur le terminal :

Output┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 0  │ front-end          │ fork     │ 0    │ online    │ 0%       │ 33.6mb   │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

Vous pouvez contrôler l'activité des processus PM2 à l'aide du stop et start commandes et les restart et startOrRestart commandes.

Vous pouvez afficher l'application en accédant à http://localhost:3000 dans votre navigateur préféré. La page d'accueil par défaut de React s'affichera :

Enfin, installez la version 5.2.0 de react-router pour le routage côté client :

npm install [email protected]

Une fois l'installation terminée, vous recevrez une variante du message suivant :

Output...

added 13 packages, and audited 1460 packages in 7s

205 packages are looking for funding
  run `npm fund` for details

6 high severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

Dans cette étape, vous configurez le squelette de votre application React dans votre conteneur Docker. Ensuite, vous allez construire les pages de votre application que vous utiliserez plus tard pour tester contre les attaques XSS.

Étape 3 - Création d'une page de connexion

Dans cette étape, vous allez créer une page de connexion pour votre application. Vous utiliserez des composants pour représenter une application avec des actifs privés et publics. Ensuite, vous implémenterez une page de connexion où un utilisateur se vérifiera pour obtenir l'autorisation d'accéder aux actifs privés sur le site Web. À la fin de cette étape, vous aurez le squelette d'une application standard avec un mélange d'actifs privés et publics et une page de connexion.

Tout d'abord, vous allez créer les pages Accueil et Connexion. Vous allez ensuite créer un SubscriberFeed composant pour représenter une page privée que seuls les utilisateurs connectés pourront voir.

Pour commencer, créez un components répertoire pour contenir tous les composants de votre application :

mkdir src/components

Ensuite, créez et ouvrez un nouveau fichier à l'intérieur du components répertoire appelé SubscriberFeed.js:

nano src/components/SubscriberFeed.js

À l'intérieur de SubscriberFeed.js fichier, ajoutez ces lignes avec un <h2> tag avec le titre du composant à l'intérieur :

jwt-storage-tutorial/front-end/src/components/SubscriberFeed.js

import React from 'react';

export default () => {
  return(
    <h2>Subscriber Feed</h2>
  );
}

Enregistrez et fermez le fichier.

Ensuite, vous allez importer le SubscriberFeed composant à l'intérieur du App.js fichier, en créant des itinéraires pour rendre le composant accessible aux utilisateurs. Ouvrez le App.js fichier trouvé dans le src répertoire de votre projet :

nano src/App.js

Ajoutez la ligne en surbrillance suivante pour importer le BrowserRouter, Switch, et Route composants de la react-router-dom forfait:

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Vous les utiliserez pour configurer le routage dans votre application Web.

Ensuite, ajoutez la ligne en surbrillance pour importer le SubscriberFeed composant que vous venez de créer :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Vous êtes maintenant prêt à créer votre application principale et les itinéraires de vos pages Web.

Toujours dedans src/App.js, supprimez les lignes JSX renvoyées (tout ce qui est contenu entre parenthèses après le return mot-clé) et remplacez-les par les lignes en surbrillance :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";

function App() {
  return(
    <div className="App">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
    </div>
  );
}

export default App;

La div l'étiquette a un className attribut de App qui contient un <h1> tag avec le nom de votre application.

Sous le <h1> tag, ajouter un BrowserRouter composant qui utilise un Switch composant pour envelopper un Route composant qui contient le SubscriberFeed composant:

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";

function App() {
  return(
    <div className="App">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

Ces nouvelles lignes vous permettent de définir les routes de votre application. La BrowserRouter Le composant contient vos chemins définis. La Switch garantit que le chemin renvoyé est le premier itinéraire qui correspond au chemin vers lequel l'utilisateur navigue, et le Route les composants définissent des noms de route spécifiques.

Enfin, vous ajouterez du rembourrage à votre application à l'aide de CSS afin que les titres et les composants soient centrés et présentables. Ajouter un wrapper au className attribut de l'extrême <div> étiquette:

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";

function App() {
  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

Enregistrez et fermez le App.js dossier.

Ouvrez le App.css dossier:

nano src/App.css

Vous verrez le CSS existant dans ce fichier. Supprimez tout dans le fichier.

Ensuite, ajoutez les lignes suivantes pour définir le wrapper coiffant:

jwt-storage-tutorial/front-end/src/App.css

.wrapper {
    padding: 20px;
    text-align: center;
}

Vous définissez le text-align propriété de la wrapper classe à center pour centrer le texte dans l'application. Vous avez également ajouté 20 pixels de rembourrage à la classe wrapper en définissant le padding propriété à 20px.

Enregistrez et fermez le App.css dossier.

Vous pouvez voir la mise à jour de votre page d'accueil React avec le nouveau style. Aller vers http://localhost:3000/subscriber-feed pour afficher le flux d'abonnés, qui est maintenant visible.

Les itinéraires fonctionnent comme prévu, mais tous les visiteurs peuvent accéder au flux d'abonnés. Pour vous assurer que le flux d'abonnés n'est visible que pour les utilisateurs authentifiés, vous devez créer une page de connexion permettant aux utilisateurs de se vérifier avec leur nom d'utilisateur et leur mot de passe.

Ouvrir un nouveau Login.js fichier dans votre répertoire de composants :

nano src/components/Login.js

Ajoutez les lignes suivantes au nouveau fichier :

jwt-storage-tutorial/front-end/src/components/Login.js

import React from 'react';

export default () => {
  return(
    <div className='login-wrapper'>
      <h1>Login</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
}

Vous créez un formulaire avec un <h1> en-tête de balise, deux entrées (username et password), et un submit bouton. Vous enveloppez le formulaire dans un <div> étiquette avec un className de login-wrapper pour que vous puissiez le coiffer dans votre App.css dossier.

Enregistrez et fermez le fichier.

Ouvrez le App.css fichier dans le répertoire racine du projet pour styliser votre Login composant:

nano src/App.css

Ajoutez les lignes CSS suivantes pour styliser le login-wrapper classer:

jwt-storage-tutorial/front-end/src/App.css

...

.login-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
}

Vous centrez les composants sur la page avec un display propriété de flex Et un align-items propriété de center. Ensuite, vous définissez le flex-direction à column, qui alignera les éléments verticalement dans une colonne.

Enregistrez et fermez le fichier.

Enfin, vous rendrez le Login composant à l'intérieur App.js en utilisant useState Hook pour stocker le jeton en mémoire. Ouvrez le App.js dossier:

nano src/App.js

Ajoutez les lignes en surbrillance au fichier :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState } from 'react'

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';

function App() {
  const [token, setToken] = useState();

  if (!token) {
    return <Login setToken={setToken} />
  }

  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

Tout d'abord, vous importez le useState crochet de la react forfait.

Vous créez également un nouveau token variable d'état pour stocker les informations de jeton qui seront récupérées lors du processus de connexion. À l'étape 5, vous améliorerez cette configuration en utilisant le stockage du navigateur pour conserver l'état d'authentification. À l'étape 7, vous renforcerez davantage votre méthode de persistance en utilisant des cookies HTTP uniquement pour stocker le statut d'authentification en toute sécurité.

Vous importez également le Login composant, qui affichera la page de connexion si la valeur de token est falsy. La if déclaration déclare que si le jeton est falsy, l'utilisateur devra se connecter s'il n'est pas authentifié. Vous passez le setToken fonction à la Login composant comme accessoire.

Enregistrez et fermez le fichier.

Ensuite, actualisez la page de votre application pour charger la page de connexion nouvellement créée. Étant donné qu'aucune fonctionnalité n'est actuellement implémentée pour définir le jeton, l'application n'affichera que la page de connexion :

Dans cette étape, vous avez mis à jour votre application avec une page de connexion et un composant privé qui sera protégé contre les utilisateurs non autorisés jusqu'à ce qu'ils se connectent.

Dans l'étape suivante, vous allez créer une nouvelle application back-end à l'aide de NodeJS et un nouveau login route pour appeler un jeton d'authentification sur votre application frontale.

Étape 4 - Création d'une API de jeton

Dans cette étape, vous allez créer un serveur Node en tant que backend de l'application React frontale que vous avez configurée à l'étape précédente. Vous utiliserez le serveur Node pour créer et mettre à disposition une API qui renvoie un jeton d'authentification lors de l'authentification réussie de l'utilisateur frontal. À la fin de cette étape, votre application disposera d'une page de connexion fonctionnelle, de ressources privées qui ne sont disponibles qu'après une authentification réussie et d'une application de serveur principal permettant l'authentification via des appels d'API.

Vous construirez le serveur en utilisant le framework Express. Vous utiliserez le cors package pour activer le partage de ressources cross-origin pour toutes les routes. Vous pouvez ensuite tester et développer votre application sans erreur CORS.

Avertissement : CORS est activé dans l'environnement de développement à des fins pédagogiques. Cependant, l'activation de CORS pour toutes les routes d'une application de production entraînera des failles de sécurité.


Créer et déplacer vers un nouveau répertoire appelé back-end qui hébergera votre projet Node :

mkdir /app/jwt-storage-tutorial/back-end
cd /app/jwt-storage-tutorial/back-end

Dans le nouveau répertoire, initialisez le projet Node :

npm init -y

La init la commande indique au npm utilitaire de ligne de commande pour créer un nouveau projet Node dans le répertoire dans lequel la commande est exécutée. La -y flag utilise les valeurs par défaut pour toutes les questions d'initialisation posées par l'outil de ligne de commande interactif lors de la création d'un nouveau projet. Voici la sortie du init commande exécutée avec le -y drapeau:

OutputWrote to /home/nodejs/jwt-storage-tutorial/back-end/package.json:

{
  "name": "back-end",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Ensuite, installez le express et cors module dans le back-end répertoire du projet :

npm install express cors

Certaines variantes de la sortie suivante apparaîtront dans le terminal :

Outputadded 59 packages, and audited 60 packages in 3s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Créer un nouveau index.js dossier:

nano index.js

Ajoutez les lignes suivantes pour importer le express module et initialiser une nouvelle application Express en appelant express() et stocker le résultat dans une variable du nom de app:

jwt-storage-tutorial/back-end/index.js

const express = require('express');
const app = express();

Ensuite, ajoutez cors à l'application en tant que middleware avec les lignes en surbrillance :

jwt-storage-tutorial/back-end/index.js

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

const app = express();

app.use(cors());

Vous importez le cors module, puis ajoutez-le au app objet avec le use méthode.

Ajoutez ensuite les lignes en surbrillance pour définir un gestionnaire pour le /login chemin qui renvoie un jeton à l'utilisateur tentant de se connecter :

jwt-storage-tutorial/back-end/index.js

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

const app = express();

app.use(cors());

app.use('/login', (req, res) => {
    res.send({
      token: "This is a secret token"
    });
});

Vous définissez un gestionnaire de requêtes pour une route avec le app.use() méthode. Cette route vous permettra d'envoyer le nom d'utilisateur et le mot de passe de l'utilisateur en cours d'authentification à partir de l'application frontale que vous venez de créer. En retour, vous fournirez le jeton d'authentification permettant à l'utilisateur d'effectuer des appels authentifiés vers l'application principale.

Le premier argument au app.use method est la route sur laquelle l'application acceptera les requêtes. Le deuxième argument est un rappel détaillant comment gérer la demande que l'application a reçue. Le rappel prend deux arguments : un req argument contenant les données de la requête et un res argument contenant les données de réponse.

Remarque : Vous ne vérifiez pas l'exactitude des informations d'identification transmises lorsque l'utilisateur demande à se connecter à l'aide de l'API principale. Cette étape n'est pas incluse par souci de concision, mais une application de production interroge normalement une base de données pour obtenir les informations de l'utilisateur afin de vérifier s'il a fourni le nom d'utilisateur et le mot de passe corrects avant d'émettre un jeton d'authentification.


Enfin, ajoutez les lignes en surbrillance pour exécuter le serveur sur le port 8080 en utilisant le app.listen fonction:

jwt-storage-tutorial/back-end/index.js

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

const app = express();

app.use(cors());

app.use('/login', (req, res) => {
    res.send({
      token: "This is a secret token"
    });
});

app.listen(8080, () => console.log(`API is active on http://localhost:8080`));

Enregistrez et fermez le fichier.

Pour exécuter votre application back-end avec PM2, créez un nouveau backend/ecosystem.config.js dossier:

nano ecosystem.config.js

Ajoutez le code de configuration suivant au nouveau back-end/ecosystem.config.js dossier:

jwt-storage-tutorial/back-end/ecosystem.config.js

module.exports = {
  apps: [
    {
      name: 'back-end',
      cwd: '/app/jwt-storage-tutorial/back-end',
      script: 'node',
      args: 'index.js',
      watch: ['index.js']
    },
  ],
};

PM2 gérera l'application back-end avec des paramètres de configuration similaires à ceux de l'application front-end.

Vous définissez le watch paramètre dans le fichier de configuration pour activer un rechargement automatique de l'application à chaque fois qu'une modification est apportée à un fichier. La watch Le paramètre est une fonctionnalité de développement utile car il met à jour les résultats dans le navigateur à mesure que des modifications sont apportées au code. Vous n'aviez pas besoin d'un paramètre watch pour l'application frontale car vous l'avez exécutée avec react-scripts, qui dispose par défaut d'une fonction de rechargement automatique. Cependant, votre application back-end sera exécutée à l'aide de la node runtime, qui n'a pas cette capacité par défaut.

Enregistrez et fermez le fichier.

Vous pouvez maintenant exécuter l'application back-end avec pm2:

pm2 start ecosystem.config.js

Votre sortie sera une variation de ce qui suit :

Output[PM2][WARN] Applications back-end not running, starting...
[PM2] App [back-end] launched (1 instances)
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 2  │ back-end           │ fork     │ 0    │ online    │ 0%       │ 24.0mb   │
│ 0  │ front-end          │ fork     │ 9    │ online    │ 0%       │ 47.2mb   │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘

Vous utiliserez curl pour évaluer si votre point de terminaison d'API nouvellement créé renvoie correctement un jeton d'authentification :

curl localhost:8080/login

Vous devriez voir la sortie suivante :

Output{"token":"This is a secret token"}

Vous savez maintenant que votre route de connexion au serveur renvoie le jeton comme prévu.

Ensuite, vous allez modifier votre front-end Login composant pour utiliser l'API. Naviguez vers le bon front-end dossier:

cd ..
cd front-end/src/components/

Ouvrir le front-end Login.js dossier:

nano Login.js

Ajoutez les lignes en surbrillance :

jwt-storage-tutorial/front-end/src/components/Login.js

import React, { useRef } from 'react';

export default () => {
  const emailRef = useRef();
  const passwordRef = useRef();

  return(
    <div className='login-wrapper'>
      <h1>Login</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" ref={emailRef} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" ref={passwordRef} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
}           

Vous ajoutez le crochet useRef pour garder une trace des valeurs des champs de saisie de l'e-mail et du mot de passe. Lors de la saisie dans les champs de saisie liés au useRef hook, les valeurs saisies seront mises à jour dans les références, qui seront ensuite envoyées au backend en appuyant sur le bouton soumettre.

Ensuite, ajoutez les lignes en surbrillance pour créer un handleSubmit rappel à gérer lorsque le bouton d'envoi est enfoncé sur le formulaire :

jwt-storage-tutorial/front-end/src/components/Login.js

import React, { useRef } from 'react';

async function loginUser(credentials) {
  return fetch('http://localhost:8080/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credentials)
  }).then(data => data.json())
}

export default ({ setToken }) => {
  const emailRef = useRef();
  const passwordRef = useRef();

  const handleSubmit = async (e) => {
    e.preventDefault();
    const token = await loginUser({
        username: emailRef.current.value,
        password: passwordRef.current.value
    })
    setToken(token)
  }

  return(
    <div className='login-wrapper'>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" ref={emailRef} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" ref={passwordRef} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
}

À l'intérieur de handleSubmit fonction de gestionnaire, vous appelez la loginUser fonction d'assistance pour faire une demande de récupération au login route de l'API créée précédemment. Appelant le preventDefault fonction sur l'événement passé dans la handleSubmit signifie que la fonctionnalité d'actualisation par défaut du bouton d'envoi n'est pas exécutée, de sorte que votre application peut à la place appeler le point de terminaison de connexion et gérer les étapes nécessaires à la connexion de l'utilisateur. Il définira également la valeur de token variable d'état en utilisant le setter passé dans le Login composant comme accessoire.

Enregistrez et fermez le fichier lorsque vous avez terminé.

Lorsque vous vérifiez l'application Web dans votre navigateur, vous pouvez désormais vous connecter avec un nom d'utilisateur et un mot de passe arbitraires. Appuyez sur le bouton Soumettre pour être redirigé vers une page sur laquelle vous êtes connecté. Si vous actualisez la page, votre application React perdra le jeton et vous serez déconnecté.

À l'étape suivante, vous utiliserez le stockage du navigateur pour conserver le jeton reçu dans l'application frontale.

Étape 5 - Stockage des jetons avec le stockage du navigateur

Cela profite à l'expérience utilisateur si les utilisateurs peuvent rester connectés pendant les sessions de navigateur et les actualisations de page. Dans cette étape, vous utiliserez la propriété Window.localStorage pour stocker les jetons d'authentification pour les sessions utilisateur persistantes qui ne sont pas perdues lorsque l'utilisateur ferme le navigateur ou actualise la page Web. Les sessions utilisateur en cours pour les applications Web modernes réduisent le trafic réseau géré par votre application, car les utilisateurs n'ont pas constamment besoin d'utiliser leurs identifiants de connexion pour le même site Web.

Le stockage du navigateur comprend deux types de stockage différents mais similaires : stockage local et stockage de session. En bref, le stockage de session conserve les données sur les sessions d'onglets tandis que le stockage local conserve les données sur les sessions d'onglets et de navigateur. Pour stocker votre jeton avec le stockage du navigateur, vous utiliserez le stockage local.

Ouvrez le App.js fichier pour votre application frontale :

nano /app/jwt-storage-tutorial/front-end/src/App.js

Pour commencer votre intégration du stockage du navigateur, ajoutez les lignes en surbrillance, qui définissent deux fonctions d'assistance (setToken et getToken), et changez le token variable pour obtenir le jeton à l'aide des fonctions nouvellement implémentées :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState } from 'react'

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';

function setToken(userToken) {
  localStorage.setItem('token', JSON.stringify(userToken));
  window.location.reload(false)
}

function getToken() {
  const tokenString = localStorage.getItem('token');
  const userToken = JSON.parse(tokenString);
  return userToken?.token
}

function App() {
  let token = getToken()

  if (!token) {
    return <Login setToken={setToken} />
  }

  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;  

Vous créez deux fonctions d'assistance : setToken et getToken. À l'intérieur de setToken, vous utilisez le setItem fonction de localStorage cartographier à partir du userToken paramètre d'entrée de la fonction d'assistance à une clé appelée token. Vous utiliserez également le reload fonction de la propriété window.location pour actualiser la page afin que votre application puisse trouver le jeton nouvellement défini dans le stockage du navigateur et restituer l'application.

À l'intérieur de getToken, vous utiliserez le getItem fonction de localStorage pour vérifier si une valeur existe pour le token clé, que vous restituerez. Vous remplacez la variable définie dans le App() fonction pour utiliser le getToken fonction.

Chaque fois que l'utilisateur visite votre site Web, l'interface vérifie s'il existe un jeton d'authentification dans le stockage du navigateur et tente de valider l'utilisateur en utilisant le jeton déjà présent plutôt que de lui demander de se connecter.

Enregistrez et fermez le fichier, puis actualisez l'application. Vous devriez maintenant pouvoir vous connecter à l'application, actualiser la page Web et ne plus avoir à vous reconnecter.

Dans cette étape, vous avez implémenté la persistance des jetons à l'aide du stockage du navigateur. Vous exploiterez le système d'authentification basé sur des jetons en utilisant le stockage du navigateur dans la section suivante.

Étape 6 - Exploitation du stockage du navigateur avec une attaque XSS

Au cours de cette étape, vous allez effectuer une attaque de script intersite (également connue sous le nom d'attaque XSS) sur votre application actuelle, qui démontrera les vulnérabilités de sécurité présentes lors de l'utilisation du stockage du navigateur pour conserver des informations secrètes. L'attaque se présentera sous la forme d'un lien URL qui, une fois cliqué, dirigera la victime vers votre application Web et injectera à l'application du code spécialement conçu. L'injection peut amener l'utilisateur à interagir avec lui, permettant à un agent malveillant de voler le contenu du stockage local sur le navigateur de la victime.

Les attaques XSS font partie des cyberattaques modernes les plus courantes. Les attaquants injectent généralement des scripts malveillants dans les navigateurs pour exécuter le code dans un environnement de confiance. Les attaquants utilisent souvent des techniques de hameçonnage pour amener les utilisateurs à compromettre le contenu de l'espace de stockage de leur navigateur en interagissant avec des liens construits de manière malveillante, tels que ceux fournis par les e-mails de spam.

Les attaques XSS présentent un intérêt particulier pour les attaquants visant à voler le contenu du stockage du navigateur d'une victime sans méfiance, car le stockage du navigateur d'un domaine est entièrement accessible au code JavaScript qui s'exécute sur tous les documents associés au domaine. Si un attaquant peut exécuter du code JavaScript sur le navigateur d'un utilisateur pour un document Web spécifique, il peut voler le contenu du stockage du navigateur de l'utilisateur (à la fois local et de session) pour le domaine Web associé à ce document du navigateur de l'utilisateur.

À des fins pédagogiques, vous laisserez intentionnellement votre application vulnérable aux attaques XSS en créant un composant appelé XSSHelper qui peut avoir du code injecté via des paramètres de requête d'URL. Vous exploiterez ensuite cette vulnérabilité en créant une URL malveillante. L'URL malveillante accédera et exposera le contenu du stockage local de l'utilisateur connecté lorsque l'utilisateur accède à l'URL dans son navigateur et clique sur un lien suspect injecté dans la page Web.

Ouvrez un nouveau composant appelé XSSHelper.js dans le components répertoire de l'application frontale :

nano /app/jwt-storage-tutorial/front-end/src/components/XSSHelper.js

Ajoutez le code suivant au nouveau fichier :

/src/components/XSSHelper.js

import React from 'react';
import { useLocation } from 'react-router-dom';

export default (props) => {
  const search = useLocation().search;
  const code = new URLSearchParams(search).get('code');


  return(
    <h2>XSS Helper Active</h2>
  );
}

Vous créez un nouveau composant fonctionnel qui importe le crochet useLocation et accède au code paramètre de requête via le search propriété de la useLocation accrocher. Vous retournez un <h2> tag avec un message indiquant que le XSSHelper composant est actif.

La fonction JavaScript URLSearchParams fournit des méthodes d'assistance telles que getters pour interagir avec les chaînes de recherche.

Ajoutez maintenant les lignes en surbrillance à importer et utilisez le useEffect crochet pour enregistrer la valeur du paramètre de requête :

/src/components/XSSHelper.js

import React, { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export default (props) => {
  const search = useLocation().search;
  const code = new URLSearchParams(search).get('code');

  useEffect(() => {
    console.log(code)
  })

  return(
    <h2>XSS Helper Active</h2>
  );
}

Enregistrez et fermez le fichier.

Ensuite, vous modifierez votre App.js fichier pour renvoyer le composant lorsque l'utilisateur accède au dossier de votre application xss-helper itinéraire.

Ouvrez le App.js dossier:

nano /app/jwt-storage-tutorial/front-end/src/App.js

Ajoutez les lignes en surbrillance à importer et ajoutez le XSSHelper composant en tant que route :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState } from 'react'

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';
import XSSHelper from './components/XSSHelper'

function setToken(userToken) {
  localStorage.setItem('token', JSON.stringify(userToken));
  window.location.reload(false)
}

function getToken() {
  const tokenString = localStorage.getItem('token');
  const userToken = JSON.parse(tokenString);
  return userToken?.token
}

function App() {
  let token = getToken()

  if (!token) {
    return <Login setToken={setToken} />
  }

  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
          <Route path="/xss-helper">
            <XSSHelper />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;  

Enregistrez et fermez le fichier.

Aller vers localhost:3000/xss-helper?code='inject code here' dans votre navigateur. Assurez-vous que vous êtes connecté à l'application, sinon vous ne pourrez pas accéder au XSSHelper composant.

Faites un clic gauche et appuyez sur Inspecter. Accédez ensuite à la section Console. Tu verras 'inject code here' dans le journal de la console.

Vous savez maintenant que vous pouvez transmettre des paramètres de requête d'URL à votre composant.

Ensuite, vous définirez la valeur des paramètres de requête transmis à votre composant sur le document de la page Web à l'aide de l'attribut dangerouslySetInnerHTML. Le composant prend la valeur de code paramètre de requête d'URL et l'injecte dans un div composant sur la page Web.

Avertissement : Utilisation du dangerouslySetInnerHTML L'attribut dans les environnements de production peut rendre votre application vulnérable aux attaques XSS.


Ouvrez le XSSHelper déposer à nouveau :

nano XSSHelper.js

Ajoutez les lignes en surbrillance :

src/components/XSSHelper.js

import React, {useEffect} from 'react';
import { useLocation } from 'react-router-dom';

export default (props) => {
  const search = useLocation().search;
  const code = new URLSearchParams(search).get('code');

  useEffect(() => {
    console.log(code)
  })

  return(
    <>
      <h2>XSS Helper Active</h2>
      <div dangerouslySetInnerHTML={{__html: code}} />
    </>
  );
}

Vous encapsulez les éléments renvoyés dans une balise JSX vide (<> ... </>) pour éviter les retours JSX multi-fragments, qui sont syntaxiquement illégaux lorsque vous travaillez avec React fragments.

Enregistrez et fermez le fichier.

Vous pouvez désormais injecter du code conçu de manière malveillante dans votre composant pour obtenir l'exécution de code sur la page Web.

Vous savez que la valeur de la code paramètre de requête envoyé au xss-helper route sera directement intégré dans le document de votre application. Vous pouvez définir la valeur de la code paramètre de requête à un lien avec un <a> balise qui utilise le href attribut pour transmettre le code JavaScript personnalisé directement au navigateur.

Accédez à l'URL suivante dans votre navigateur :

localhost:3000/xss-helper?code=<a href="javascript:alert(`You have been pwned`);">Click Me!</a>

Dans l'URL ci-dessus, vous créez une charge utile XSS de paramètre de requête pour qu'elle s'affiche sous la forme d'un lien qui lit Click Me! sur la page Web. Lorsque l'utilisateur clique sur le lien, le lien indique au navigateur d'exécuter votre code JavaScript spécialement conçu. Ce code utilise le alert fonction pour créer un pop-up avec le message Vous avez été pwned.

Ensuite, accédez à l'URL suivante dans votre navigateur :

localhost:3000/xss-helper?code=<a href="javascript:alert(`Your token object is ${localStorage.getItem('token')}. It has been sent to a malicious server >:)`);">Click Me!</a>

Pour cette page, le contenu du stockage du navigateur est accessible à un attaquant via l'injection de script de paramètre de requête d'URL qui lit la valeur du token stocké dans localStorage avec du code Javascript.

Vous devez être connecté à l'application pour que le jeton existe, ce qui permet à votre URL conçue de manière malveillante d'afficher le jeton stocké dans le stockage local. Lorsque vous appuyez sur le lien Click Me! sur la page Web, vous recevez un message contextuel indiquant que votre jeton a été volé.

Dans cette étape, vous avez utilisé l'un des nombreux exemples de vecteurs d'attaque pour réaliser l'exécution du code. Avec le jeton d'authentification d'un utilisateur sans méfiance, les attaquants malveillants peuvent usurper l'identité des utilisateurs sur votre application Web pour accéder aux ressources du site privilégié. Grâce à ces tests, vous savez maintenant que le stockage d'informations secrètes, telles que des jetons d'authentification, dans le stockage du navigateur est une pratique dangereuse.

Ensuite, vous utiliserez une méthode alternative pour stocker des informations secrètes, qui seront inaccessibles aux scripts exécutés sur le document et immunisées contre ce type d'attaque XSS.

Étape 7 - Utilisation de cookies HTTP uniquement pour atténuer la vulnérabilité XSS de stockage du navigateur

Dans cette étape, vous utiliserez des cookies HTTP uniquement pour atténuer la vulnérabilité XSS découverte et exploitée à l'étape précédente.

Les cookies HTTP sont des extraits d'informations stockés dans des paires clé-valeur dans le navigateur. Ils sont souvent utilisés pour le suivi, la personnalisation ou la gestion de session.

JavaScript ne peut pas accéder à un cookie HTTP uniquement via le Document.cookie propriété, qui aide à prévenir les attaques XSS visant à voler des informations utilisateur par injection de code malveillant. Vous pouvez utiliser l'en-tête Set-Cookie pour définir des cookies côté serveur pour les clients authentifiés, qui seront disponibles dans chaque requête que le client adresse au serveur et pourront ensuite être utilisés par le serveur pour vérifier l'état de l'authentification. de l'utilisateur. Vous utiliserez le cookie-parser middleware avec Express pour gérer cela plutôt que de définir l'en-tête.

Pour mettre en œuvre un stockage de jetons sécurisé basé sur des cookies HTTP uniquement, vous allez mettre à jour les fichiers suivants :

  • Le back-end index.js fichier sera modifié pour implémenter le login route afin qu'il définisse un cookie une fois l'authentification réussie. Le back-end aura également besoin de deux nouvelles routes : une pour vérifier l'état d'authentification d'un utilisateur et une pour déconnecter un utilisateur.
  • Le frontal Login.js et App.js les fichiers seront modifiés pour utiliser les nouvelles routes du back-end.

Ces modifications implémenteront les fonctionnalités de connexion, de déconnexion et d'état d'authentification à votre code client et serveur.

Déplacez-vous vers le répertoire principal et installez le cookie-parser package, qui vous permettra de définir et de lire les cookies dans votre application Express :

cd /app/jwt-storage-tutorial/back-end
npm install cookie-parser

Vous verrez une variante de la sortie suivante :

Output...
added 2 packages, and audited 62 packages in 1s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities...

Ensuite, ouvrez index.js dans votre application back-end :

nano /app/jwt-storage-tutorial/back-end/index.js

Ajoutez le code en surbrillance pour importer le nouveau cookie-parser paquet avec le require et utilisez-la comme middleware dans l'application :

backend/index.js

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

const cookieParser = require('cookie-parser')

const app = express();

app.use(cors());
app.use(cookieParser())

app.post('/login', (req, res) => {
    res.send({
      token: "This is a secret token"
    });
});

app.listen(8080, () => console.log('API active on http://localhost:8080'));

Vous configurerez également le cors middleware pour contourner les restrictions CORS à des fins de développement. Dans le même fichier, ajoutez les lignes en surbrillance :

backend/index.js

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

const cookieParser = require('cookie-parser')

const app = express();

let corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true,
}

app.use(cors(corsOptions));
app.use(cookieParser())

app.post('/login', (req, res) => {
    res.send({
      token: "This is a secret token"
    });
});

app.listen(8080, () => console.log('API active on http://localhost:8080'));

Vous définissez le Access-Control-Allow-Origin en-tête CORS à l'aide de origin option dans le cadre de la corsOptions objet au domaine à partir duquel votre frontal envoie des requêtes API. Vous définissez également le credentials paramètre à true, qui indique au serveur frontal qu'il est censé envoyer le jeton d'autorisation dans un cookie pour chaque demande d'API. La valeur de la origin L'option spécifie à partir de quels domaines accepter les données de contrôle d'accès, telles que les cookies, pour le traitement back-end.

Enfin, vous passez dans le corsOptions objet de configuration dans le cors objet middleware.

Ensuite, vous définirez le jeton de cookie de l'utilisateur à l'aide de la cookie() méthode mise à disposition sur l'objet de réponse de votre gestionnaire d'itinéraire par le cookie-parser middleware. Remplacez les lignes dans le app.use('/login', (req, res) section avec les lignes en surbrillance :

backend/index.js

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

const cookieParser = require('cookie-parser')

const app = express();

let corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true,
}

app.use(cors(corsOptions));
app.use(cookieParser())

app.use('/login', (req, res) => {
    res.cookie("token", "this is a secret token", {
      httpOnly: true,
      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
      domain: "localhost",
      sameSite: 'Lax',
    }).send({
      authenticated: true,
      message: "Authentication Successful."});
});

app.listen(8080, () => console.log('API active on http://localhost:8080'));

Dans le bloc de code ci-dessus, vous définissez un cookie avec une clé de token et une valeur de this is a secret token. La httpOnly L'option de configuration définit le httpOnly afin que le cookie ne soit pas accessible au JavaScript exécuté sur le document.

Vous définissez le maxAge attribut afin que le cookie expire dans 14 jours. Après 14 jours, le cookie expirera et le navigateur aura besoin d'un nouveau cookie d'authentification. L'utilisateur devra donc se reconnecter avec son nom d'utilisateur et son mot de passe.

La sameSite et domain Les attributs sont définis pour garantir que le navigateur client ne rejette pas vos cookies en raison de CORS ou d'autres problèmes de protocole de sécurité.

Maintenant que vous avez un itinéraire pour vous connecter, vous avez besoin d'un itinéraire pour vous déconnecter. Ajoutez les lignes en surbrillance pour définir la méthode de déconnexion :

backend/index.js

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

const cookieParser = require('cookie-parser')

const app = express();

let corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true,
}

app.use(cors(corsOptions));
app.use(cookieParser())

app.use('/login', (req, res) => {
    res.cookie("token", "this is a secret token", {
      httpOnly: true,
      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
      domain: "localhost",
      sameSite: 'Lax',
    }).send({
      authenticated: true,
      message: "Authentication Successful."});
});

app.use('/logout', (req, res) => {
  res.cookie("token", null, {
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
    domain: "localhost",
    sameSite: 'Lax',
  }).send({
    authenticated: false,
    message: "Logout Successful."
  });
});

app.listen(8080, () => console.log('API active on http://localhost:8080'));

La logout méthode est similaire à la login itinéraire. La logout supprimera le jeton que l'utilisateur a stocké en tant que cookie en définissant la token biscuit à null. Ensuite, il informera l'utilisateur qu'il a été déconnecté avec succès.

Enfin, ajoutez les lignes en surbrillance pour implémenter un auth-status route qui permet au client utilisateur de vérifier si l'utilisateur est connecté ou non et autorisé à accéder aux actifs privés :

backend/index.js

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

const cookieParser = require('cookie-parser')

const app = express();

let corsOptions = {
  origin: 'http://localhost:3000',
  credentials: true,
}

app.use(cors(corsOptions));
app.use(cookieParser())

app.use('/login', (req, res) => {
    res.cookie("token", "this is a secret token", {
      httpOnly: true,
      maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
      domain: "localhost",
      sameSite: 'Lax',
    }).send({
      authenticated: true,
      message: "Authentication Successful."});
});

app.use('/logout', (req, res) => {
  res.cookie("token", null, {
    httpOnly: true,
    maxAge: 1000 * 60 * 60 * 24 * 14, // 14 Day Age,
    domain: "localhost",
    sameSite: 'Lax',
  }).send({
    authenticated: false,
    message: "Logout Successful."
  });
});

app.use('/auth-status', (req, res) => {
  console.log(req.cookies)

  if (req.cookies?.token === "this is a secret token") {
    res.send({isAuthenticated: true})
  } else {
    res.send({isAuthenticated: false})
  }
})

app.listen(8080, () => console.log('API active on http://localhost:8080'));

Ton auth-status vérifications d'itinéraire pour un token cookie qui correspond à la valeur attendue pour le jeton d'authentification de l'utilisateur. Il répond ensuite par une valeur booléenne pour indiquer si l'utilisateur est authentifié ou non.

Enregistrez et fermez le fichier lorsque vous avez terminé. Vous avez apporté les modifications nécessaires à votre backend pour permettre à votre frontend de suivre le statut d'authentification de l'utilisateur via votre API backend.

Ensuite, vous apporterez les modifications frontales nécessaires pour implémenter le stockage de jetons basé sur des cookies HTTP uniquement.

Déplacez-vous vers le front-end répertoire et ouvrez le Login.js dossier:

cd ..
cd front-end/src/components/
nano Login.js

Ajoutez la ligne en surbrillance pour modifier le loginUser fonction dans votre Login composant:

jwt-storage-tutorial/front-end/src/components/Login.js

...

async function loginUser(credentials) {
  return fetch('http://localhost:8080/login', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credentials)
  }).then(data => data.json())
}

...            

Vous définissez le credentials en-tête de vos demandes de récupération à include, qui raconte le loginUser fonction pour envoyer toutes les informations d'identification qui pourraient être définies comme cookies sur les appels d'API au login chemin que vous venez de modifier sur votre back-end.

Ensuite, vous supprimerez le setToken propriété d'entrée à la Login composant et son utilisation en fin de handleSubmit rappel puisque vous ne garderez plus de jeton en mémoire.

Vous devrez également déclencher un rafraîchissement à la fin de la handlesubmit fonction afin que votre application se rafraîchisse en cliquant sur le bouton de connexion et le nouveau jeu token cookie est reconnu par l'application cliente. Ajoutez la ligne en surbrillance :

jwt-storage-tutorial/front-end/src/components/Login.js

...
  const handleSubmit = async (e) => {
    e.preventDefault();
    const token = await loginUser({
        username: emailRef.current.value,
        password: passwordRef.current.value
    })
    window.location.reload(false);
  }
...

Ton Login.js le fichier devrait maintenant ressembler à ceci :

jwt-storage-tutorial/front-end/src/components/Login.js

import React, { useRef } from 'react';

async function loginUser(credentials) {
  return fetch('http://localhost:8080/login', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credentials)
  }).then(data => data.json())
}

export default () => {
  const emailRef = useRef();
  const passwordRef = useRef();

  const handleSubmit = async (e) => {
    e.preventDefault();
    const token = await loginUser({
        username: emailRef.current.value,
        password: passwordRef.current.value
    })
    window.location.reload(false);
  }

  return(
    <div className='login-wrapper'>
      <h1>Login</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" ref={emailRef} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" ref={passwordRef} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  );
}

Enregistrez et fermez le fichier.

Étant donné que vous ne conservez plus le jeton d'authentification en mémoire, vous ne pouvez pas vérifier si vous avez un jeton d'authentification présent lorsque vous devez déterminer si l'utilisateur doit se connecter ou s'il peut accéder à des actifs privés.

Pour effectuer ces modifications, ouvrez le App.js fichier pour votre front-end :

cd ..
nano App.js

Importez le useState crochet de la react package et initialiser un nouveau authenticated variable d'état et son setter pour refléter le statut d'authentification de l'utilisateur :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState } from 'react'

...

function App() {
  let [authenticated, setAuthenticated] = useState(false);

  if (!token) {
    return <Login setToken={setToken} />
  }

  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

La useState hook vérifiera le statut d'authentification de l'utilisateur en adressant une requête à votre API back-end qui peut indiquer si un jeton d'authentification valide est activement détenu en tant que cookie pour le client frontal.

Ensuite, retirez le setToken et getToken fonctions, la variable de jeton et le rendu conditionnel de la login composant. Ensuite, créez deux nouvelles fonctions appelées getAuthStatus et isAuthenticated avec les lignes en surbrillance :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState } from 'react'

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';

function App() {
  let [authenticated, setAuthenticated] = useState(false);

  async function getAuthStatus() {
    return fetch('http://localhost:8080/auth-status', {
      method: 'GET',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    }).then(data => data.json())
  }

  async function isAuthenticated() {
    const authStatus = await getAuthStatus();
    setAuthenticated(authStatus.isAuthenticated);
  }

  return(
    <div className="App wrapper">
      <h1 className="App-header">
        JWT-Storage-Tutorial Application
      </h1>
      <BrowserRouter>
        <Switch>
          <Route path="/subscriber-feed">
            <SubscriberFeed />
          </Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;    

La getAuthStatus fonction fera un GET demande au auth-status route de votre application back-end pour récupérer le statut d'authentification de l'utilisateur, en attendant que l'utilisateur ait envoyé ou non la demande avec un cookie de jeton d'authentification valide.

En fixant la valeur de credentials possibilité de include, fetch enverra toutes les informations d'identification que le navigateur peut stocker pour le client utilisateur sous forme de cookies. La isAuthenticated la fonction appellera le getAuthStatus fonction et réglez la authenticated l'état de votre application à une valeur booléenne reflétant le statut d'authentification de l'utilisateur.

Ensuite, vous allez importer le useEffect crochet avec les lignes en surbrillance :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState, useEffect } from 'react'

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';

function App() {
  let [authenticated, setAuthenticated] = useState(false);

  async function getAuthStatus() {
    return fetch('http://localhost:8080/auth-status', {
      method: 'GET',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    }).then(data => data.json())
  }

  async function isAuthenticated() {
    const authStatus = await getAuthStatus();
    setAuthenticated(authStatus.isAuthenticated)
  }

  useEffect(() => {
    isAuthenticated();
  }, [])

...

Cette modification appellera le login route pour vérifier l'état de l'authentification dans le useEffect accrocher. Inclure un tableau de dépendances vide pour le useEffect hook peut aider à éviter les fuites de mémoire dans votre application.

Pour rendre conditionnellement le login composant sur la page d'accueil de l'application, ajoutez les lignes en surbrillance :

jwt-storage-tutorial/front-end/src/App.js

...

function App() {
  let [authenticated, setAuthenticated] = useState(false);
  let [loading, setLoading] = useState(true)

  async function getAuthStatus() {
    await setLoading(true);
    return fetch('http://localhost:8080/auth-status', {
      method: 'GET',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    }).then(data => data.json())
  }

  async function isAuthenticated() {
    const authStatus = await getAuthStatus();
    await setAuthenticated(authStatus.isAuthenticated);
    await setLoading(false)
  }

  useEffect(() => {
    isAuthenticated();
  }, [])

  return (
    <>
      {!loading && (
        <>
          {!authenticated && <Login />}

          {authenticated && (
            <div className="App wrapper">
              <h1 className="App-header">
                JWT-Storage-Tutorial Application
              </h1>
              <BrowserRouter>
                <Switch>
                  <Route path="/subscriber-feed">
                    <SubscriberFeed />
                  </Route>
                  <Route path="/xss-helper">
                    <XSSHelper />
                  </Route>
                </Switch>
              </BrowserRouter>
            </div>
          )}
        </>
      )}
    </>
  );
}

export default App;

Si la authenticated variable est définie sur false, votre application rendra le login composant. Sinon, la page d'accueil de l'application et toutes ses routes, y compris les pages privées, s'afficheraient à la place.

Vous ajoutez un nouveau loading variable d'état pour éviter de rendre quoi que ce soit jusqu'à l'appel à la auth-status route de votre application back-end est terminée. Parce que le authenticated la variable d'état est initialement définie sur false, le client supposera que l'utilisateur ne s'est pas connecté jusqu'à l'appel de l'API au authentication-status l'itinéraire est terminé et le authenticated la variable d'état est mise à jour.

Vous allez ensuite créer un logoutUser fonction qui appelle votre logout route sur l'API back-end. Ajoutez les lignes en surbrillance au fichier :

jwt-storage-tutorial/front-end/src/App.js

import logo from './logo.svg';
import './App.css';

import { useState, useEffect } from 'react';

import { BrowserRouter, Route, Switch } from 'react-router-dom';
import SubscriberFeed from "./components/SubscriberFeed";
import Login from './components/Login';
import XSSHelper from './components/XSSHelper'

function App() {
  let [authenticated, setAuthenticated] = useState(false);
  let [loading, setLoading] = useState(true)

  async function getAuthStatus() {
    await setLoading(true);
    return fetch('http://localhost:8080/auth-status', {
      method: 'GET',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    }).then(data => data.json())
  }

  async function isAuthenticated() {
    const authStatus = await getAuthStatus();
    await setAuthenticated(authStatus.isAuthenticated);
    await setLoading(false);
  }

  async function logoutUser() {
    await fetch('http://localhost:8080/logout', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
    })
    isAuthenticated();
  }

  useEffect(() => {
    isAuthenticated();
  }, [])

  return (
    <>
      {!loading && (
        <>
          {!authenticated && <Login />}

          {authenticated && (
            <div className="App wrapper">
              <h1 className="App-header">
                JWT-Storage-Tutorial Application
              </h1>
              <button onClick={logoutUser}>Logout</button>
              <BrowserRouter>
                <Switch>
                  <Route path="/subscriber-feed">
                    <SubscriberFeed />
                  </Route>
                  <Route path="/xss-helper">
                    <XSSHelper />
                  </Route>
                </Switch>
              </BrowserRouter>
            </div>
          )}
        </>
      )}
    </>
  );
}

export default App;

Vous allez créer un bouton de déconnexion pour déconnecter l'utilisateur, en définissant son onClick attribut à une fonction de rappel qui appelle votre logout route sur l'API back-end. L'itinéraire répondra par un set-cookie en-tête qui définit le token cookie du client à null, rendant ainsi le statut d'authentification de votre application frontale à un falsy évaluer.

Vous appellerez également le isAuthenticated fonction à la fin du logout fonction de rappel, qui mettra à jour le statut de votre application pour refléter le statut non authentifié de l'utilisateur en définissant le authenticated variable d'état à false.

Enregistrez et fermez le fichier lorsque vous avez terminé.

Vous pouvez maintenant tester le système de stockage de jetons basé sur des cookies HTTP uniquement. Actualisez l'application Web pour mettre en œuvre les modifications que vous venez d'apporter.

Ensuite, effacez le contenu du stockage de votre navigateur pour supprimer tous les jetons persistants dans le stockage du navigateur. Ensuite, accédez à la même URL construite de manière malveillante qu'à l'Étape 4 pour voir si un attaquant peut toujours voler votre jeton via du JavaScript injecté :

localhost:3000/xss-helper?code=<a href="javascript:alert(`Your token object is ${localStorage.getItem('token')}. It has been sent to a malicious server >:)`);">Click Me!</a>

Vous devrez peut-être vous reconnecter à votre site pour voir le XSS Helper Active ligne. Vous devriez voir la fenêtre contextuelle suivante indiquant Your token object is null après avoir cliqué sur le lien qui lit Click Me!:

Le JavaScript injecté ne peut pas trouver le token objet, de sorte que la fenêtre contextuelle affiche un null évaluer. Fermez le message contextuel.

Vous devriez maintenant pouvoir vous déconnecter de l'application en appuyant sur le bouton Logout.

Au cours de cette étape, vous avez amélioré la sécurité de votre application en passant de l'utilisation du stockage du navigateur pour la persistance du jeton d'authentification à l'utilisation de cookies HTTP uniquement.

Conclusion

Dans ce didacticiel, vous avez créé une application Web React et Node avec une fonctionnalité de connexion utilisateur dans un conteneur Docker. Vous avez mis en place un système d'authentification avec une méthode de stockage de jetons vulnérables pour tester la sécurité de votre site. Vous avez ensuite exploité cette méthode avec une charge utile d'attaque XSS réfléchie, vous permettant d'évaluer les vulnérabilités lors de l'utilisation du stockage du navigateur pour stocker les cookies d'authentification. Enfin, vous avez atténué la vulnérabilité XSS dans l'implémentation initiale en configurant un système d'authentification qui utilise des cookies HTTP uniquement plutôt que le stockage du navigateur pour stocker les jetons d'authentification. Vous disposez maintenant d'une application frontale et dorsale avec un système de jeton d'authentification basé sur des cookies HTTP uniquement.

Pour améliorer la sécurité et la convivialité du processus d'authentification de votre application, vous pouvez intégrer des outils d'authentification tiers tels que PassportJS ou une API OAuth, telle que API OAuth de DigitalOcean. . Pour en savoir plus sur le framework OAuth, vous pouvez consulter An Introduction to OAuth 2.