Sécurisation d'une application monopage (SPA) - Intégration avancée

Cet article présente une approche permettant d'intégrer CrowdHandler à une application monopage (SPA) afin de la protéger contre un trafic excessif et de garantir une expérience utilisateur fluide. L'intégration repose sur deux composants principaux :

  1. Intégration JavaScript de CrowdHandler avec le mode SPA activé.

  2. Une intégration personnalisée côté serveur protégeant l'API (ou les API) qui alimentent votre SPA.

Le rôle de l'intégration JavaScript est de constituer la première et principale couche de protection ; elle est chargée de vérifier les requêtes des utilisateurs, de gérer l'état de la promotion au sein du navigateur et de rediriger les utilisateurs vers la salle d'attente si nécessaire.

Le rôle de l'intégration côté serveur est de servir de deuxième niveau de protection, afin d'empêcher toute personne suffisamment experte de contourner l'intégration JavaScript, tout en étant chargée de fournir des informations sur les performances à CrowdHandler.

Installation de l'intégration JavaScript

La première étape consiste à installer notre intégration JavaScript avec le mode SPA activé

Par défaut, les vérifications CrowdHandler n'ont lieu qu'à l'occasion d'un rechargement complet du DOM, c'est-à-dire lorsque le navigateur est actualisé de manière forcée ou lorsque la page est récupérée pour la première fois depuis votre serveur web, avant le téléchargement du bundle de l'application. Dans les applications SPA, cela a pour effet que les utilisateurs ne sont plus visibles par CrowdHandler après leur première visite.

Le mode SPA résout ce problème en activant des fonctionnalités supplémentaires qui permettent d'effectuer des vérifications CrowdHandler à chaque changement d'URL, qu'il y ait eu ou non un rafraîchissement du DOM. Pour ce faire, il suit l'état de l'URL et utilise un écouteur d'événements pour forcer une vérification CrowdHandler dès qu'un changement est détecté.

Protéger vos API

La manière précise dont vous protégez votre API avec CrowdHandler dépend du langage ou du framework que vous utilisez ; il est donc impossible de couvrir tous les cas de figure dans ce guide. Vous trouverez des liens vers des exemples de mise en œuvre spécifiques pour les environnements NodeJS et Lambda@Edge (CloudFront) à la fin de cet article.

1. Ajoutez des champs supplémentaires aux données de votre requête API.

Pour les besoins de cet exemple, imaginons que vous gériez une application SPA de commerce électronique. Il existe une seule API, sous votre contrôle, qui est appelée pour récupérer des données.

Nous allons partir des hypothèses suivantes, mais nos exemples peuvent facilement être adaptés à vos besoins :

  • Les données utiles sont envoyées avec le type de contenu « application/json ».
  • Vous souhaitez uniquement protéger les méthodes PUT et POST. Cela couvre généralement des opérations telles que « Ajouter au panier » et « Passer à la caisse », ce qui est suffisant pour empêcher les personnes qui contournent l'intégration JavaScript de mener à bien des parcours complets. *

* Rien ne vous empêche de protéger tous les types d'appels API et de méthodes de requête ; cela peut s'avérer judicieux si vous craignez que des acteurs malveillants ne ciblent, par exemple, des routes API à forte charge qui répondent à des méthodes GET. Vous devrez alors ajouter et extraire les champs supplémentaires sous forme de paramètres de chaîne de requête.

Clé : sourceURL

Valeur : location.href (ou équivalent)

Clé : chToken

Valeur : stockage local jeton Crowdhandler *

* Voici un exemple simple de fonction qui extrait le jeton CrowdHandler du stockage local. Remplacez « my.domain.com » par le domaine de votre site et renvoyez une chaîne vide « » si aucun jeton n'est trouvé. Ceci est important, car cela indique au code côté serveur qu'une nouvelle session CrowdHandler doit être attribuée.

//Storage format
'{"countdown":{},"positions":{},"token":{"my.domain.com":"tok0N53DjDMpWeid"}}'
try {
  let ch_storage = JSON.parse(localStorage.getItem("crowdhandler"))
  return ch_storage.token["my.domain.com"]
} catch (error) {
  return ""
}

2. Installer côté serveur#

Le code côté serveur a pour but de se placer en amont de votre API et de valider les requêtes auprès de CrowdHandler afin de vérifier leur état de promotion. Les appels API qui ne présentent pas de session CrowdHandler promue doivent être immédiatement bloqués.

La valeur « sourceURL » que vous avez fournie dans vos charges utiles API est utilisée comme URL temporaire lors de la connexion à CrowdHandler. Dans le panneau de configuration, vous avez configuré CrowdHandler pour protéger les URL de votre site web, et non celles de votre API. Cette réécriture temporaire utilisant la valeur « sourceURL » indique à CrowdHandler la page d'où provient l'appel API.

Le jeton CrowdHandler est extrait de la valeur « chToken » que vous avez fournie dans vos charges utiles API.

Pour plus de détails sur la mise en œuvre, consultez les commentaires du code.

Exemple - Express Framework

const express = require("express");
const router = express.Router();
const crowdhandler = require("crowdhandler-sdk");
const { URL } = require("url");

// Middleware to handle CrowdHandler logic for POST and PUT methods
const crowdHandlerMiddleware = async (req, res, next) => {
  const method = req.method;

  // Check if the request method is POST or PUT
  if (method === "POST" || method === "PUT") {
    const publicKey = "YOUR_PUBLIC_KEY";
    const public_client = new crowdhandler.PublicClient(publicKey);
    const ch_context = new crowdhandler.RequestContext({request: req, response: res});
    const ch_gatekeeper = new crowdhandler.Gatekeeper(
      public_client,
      ch_context,
      { publicKey: publicKey }
    );

    let decodedBody;
    let chToken;
    let sourceURL;

    if (req.body) {
      try {
        decodedBody = JSON.parse(req.body);
        chToken = decodedBody.chToken;
        sourceURL = decodedBody.sourceURL;

        // Extract host & path from sourceURL
        let url = new URL(sourceURL);
        let temporaryHost = url.host;
        let temporaryPath = url.pathname;

        // Override the gatekeeper host and path with the sourceURL
        ch_gatekeeper.overrideHost(temporaryHost);
        ch_gatekeeper.overridePath(temporaryPath);

        // If there's a token in the body, provide gatekeeper with a pseudo cookie
        if (chToken) {
          ch_gatekeeper.overrideCookie(`crowdhandler=${chToken}`);
        }
      } catch (error) {
        console.error("Error parsing JSON:", error);
        return next(error);
      }
    }

    const ch_status = await ch_gatekeeper.validateRequest();

    // If the request is not promoted, send a 403 Forbidden response and do not proceed to the next middleware
    if (!ch_status.promoted) {
      res.status(403).send("Forbidden");
      return;
    } else {
      // If the request is promoted, save the ch_gatekeeper instance in res.locals for later use
      res.locals.ch_gatekeeper = ch_gatekeeper;
    }
  }
  // Continue to the next middleware or route handler
  next();
};

// Add the CrowdHandler middleware to the router
router.use(crowdHandlerMiddleware);

// Route handler for all request methods and paths
router.all("*", (req, res, next) => {
  // Render the view and send the HTML
  res.render("index", { title: "hello" }, (err, html) => {
    // Handle any errors during rendering
    if (err) {
      return next(err);
    }

    // Send the rendered HTML to the client
    res.send(html);

    // If the ch_gatekeeper instance exists in res.locals, record the performance
    if (res.locals.ch_gatekeeper) {
      res.locals.ch_gatekeeper.recordPerformance();
    }

    /*
     * IMPORTANT CONSIDERATION:
     *
     * The default status code sent to CrowdHandler is '200'. However, if a different status code needs to be sent,
     * it can be achieved by passing it as a parameter to the 'recordPerformance' method.
     *
     * Example:
     * chGatekeeper.recordPerformance({status: 404});
     *
     * If you are using CrowdHandler's autotune feature, is is crucial to pass accurate status codes to CrowdHandler to ensure the precision of analytics and autotune results.
     */
  });
});

// Export the router
module.exports = router;

Exemple - Lambda@Edge

Demande d'un téléspectateur

"use strict";
//include crowdhandler-sdk
const crowdhandler = require("crowdhandler-sdk");
const publicKey = "YOUR_PUBLIC_KEY_HERE";

let ch_client = new crowdhandler.PublicClient(publicKey, { timeout: 2000 });

module.exports.viewerRequest = async (event) => {
  //extract the request from the event
  let request = event.Records[0].cf.request;
  let decodedBody;
  let chToken;
  let sourceURL;

  //if the request is not a POST or PUT request, return the request unmodified
  if (request.method !== "POST" || request.method !== "PUT" ) {
    return request;
  }

  if (request.body && request.body.encoding === "base64") {
    // Decode the base64 encoded body
    decodedBody = Buffer.from(request.body.data, "base64").toString("utf8");

    // Parse the JSON encoded body
    try {
      // Parse the decoded body into a JSON object
      decodedBody = JSON.parse(decodedBody);
      //destructure sourceURL, chToken from the decoded body
      chToken = decodedBody.chToken;
      sourceURL = decodedBody.sourceURL;

      // Now you can work with the JSON object
    } catch (error) {
      console.error("Error parsing JSON:", error);

      // Handle the error or return the request object unmodified
      return request;
    }
  }

  //extract host & path from sourceURL using URL API
  let url = new URL(sourceURL);
  let temporaryHost = url.host;
  let temporaryPath = url.pathname;

  //Filter the event through the Request Context class
  let ch_context = new crowdhandler.RequestContext({ lambdaEvent: event });
  //Instantiate the Gatekeeper class
  let ch_gatekeeper = new crowdhandler.Gatekeeper(
    ch_client,
    ch_context,
    {
      publicKey: publicKey,
    },
    { debug: true }
  );

  //Override the gatekeeper host with the sourceURL
  ch_gatekeeper.overrideHost(temporaryHost);
  //Override the gatekeeper path with the sourceURL
  ch_gatekeeper.overridePath(temporaryPath);

  //If there's a token in the body provide gatekeeper with a pseudo cookie so that it can check that the provided token is valid/promoted
  if (chToken) {
    ch_gatekeeper.overrideCookie(`crowdhandler=${chToken}`);
  }

  //Validate the request
  let ch_status = await ch_gatekeeper.validateRequest();

  //If the request is not promoted, reject the request
  if (!ch_status.promoted) {
    return {
      status: "403",
      statusDescription: "Forbidden",
      headers: {
        "content-type": [
          {
            key: "Content-Type",
            value: "text/plain",
          },
        ],
        "cache-control": [
          {
            key: "Cache-Control",
            value: "max-age=0",
          },
        ],
      },
      body: "Access to this resource is forbidden.",
    };
  }

  //If the request is promoted, allow it to proceed normally
  //set customer headers for recording performance on the request before passing it through
  request.headers["x-crowdhandler-responseID"] = [
    { key: "x-crowdhandler-responseID", value: `${ch_status.responseID}` },
  ];
  request.headers["x-crowdhandler-startTime"] = [
    { key: "x-crowdhandler-startTime", value: `${Date.now()}` },
  ];

  //return the request
  return request;
};

Réponse d'Origin

const crowdhandler = require("crowdhandler-sdk");
const publicKey = "YOUR_PUBLIC_KEY_HERE";

let ch_client = new crowdhandler.PublicClient(publicKey, { timeout: 2000 });

module.exports.originResponse = async (event) => {
  let request = event.Records[0].cf.request;
  let requestHeaders = event.Records[0].cf.request.headers;
  let response = event.Records[0].cf.response;
  let responseStatus = response.status;

  //convert response status to number
  responseStatus = parseInt(responseStatus);

  //extract the custom headers that we passed through from the viewerRequest event
  let responseID;
  let startTime;

  try {
    responseID = requestHeaders["x-crowdhandler-responseid"][0].value;
  } catch (e) {}

  try {
    startTime = requestHeaders["x-crowdhandler-starttime"][0].value;
  } catch (e) {}

  //Work out how long we spent processing at the origin
  let elapsed = Date.now() - startTime;

  let ch_context = new crowdhandler.RequestContext({ lambdaEvent: event });

  //Instantiate the Gatekeeper class
  let ch_gatekeeper = new crowdhandler.Gatekeeper(
    ch_client,
    ch_context,
    {
      publicKey: publicKey,
    },
    { debug: true }
  );

  //If we don't have a responseID or a startTime, we can't record the performance
  if (!responseID || !startTime) {
    return response;
  }

  //This is a throw away request. We don't need to wait for a response.
  await ch_gatekeeper.recordPerformance({
    overrideElapsed: elapsed,
    responseID: responseID,
    sample: 1,
    statusCode: responseStatus,
  });

  //Fin
  return response;
};

3. Allons plus

Les exemples ci-dessus constituent des solutions relativement simples pour bloquer l'accès à votre API aux utilisateurs considérés comme non autorisés par CrowdHandler.

Si vous souhaitez offrir une expérience optimale aux utilisateurs qui accèdent directement à vos API ou si vous souhaitez prendre en compte les cas particuliers, vous pouvez modifier le code d'exemple afin qu'il renvoie une réponse JSON contenant une URL de salle d'attente complète. Consultez notre documentation sur le SDK JS pour savoir comment obtenir cette URL.

Une fois que vous disposez de l'URL complète de la salle d'attente, vous pouvez l'afficher dans la réponse et demander à votre code côté client de remplacer l'URL actuelle par celle de la salle d'attente.

N'oubliez pas ! Cette opération doit être effectuée côté client ; réécrire les requêtes API côté serveur revient en effet à renvoyer une réponse 403, ce qui redirigera les appels API, et non le navigateur de l'utilisateur.

4.) finales

Même si nous espérons que les exemples fournis sont clairs et utiles, nous comprenons que vous ayez parfois besoin de vous adresser à un spécialiste pour obtenir des conseils ou des précisions. Nos experts en intégration sont joignables à l'adresse support@crowdhandler.com et se tiennent à votre disposition pour vous aider si nécessaire.