Utiliser Offline Application Cache avec une authentification

La spécification HTML5 a apporté beaucoup de choses dont le Offline Application Cache (que j’abrègerais en AppCache). Ce système permet, au travers d'un manifeste, de placer dans un cache spécial des fichiers qui ne seront plus requêté par la suite jusqu'à ce que le manifeste soit modifié.

C'est très pratique pour fabriquer une version hors-ligne du site ou bien économiser la bande passante en empêchant certains éléments statiques d'être requêté par le navigateur.

Maintenant que se passe-t-il si vous (ou le client) avez la lubie de vouloir sécuriser le contenu du site tout en conservant ce système de mise en cache ?

Pré-requis

  • Cet tutoriel suppose que vous savez mettre en place l'AppCache. Si ce n'est pas le cas je vous conseilles de lire cet article et d'approfondir le sujet avec celui-ci.
  • Ne sera pas non plus abordé la manière dont vous comptez sécuriser votre site. Il sera assumé que l'application possède une page d'authentification et qu'elle possède une API capable de déterminer si une requête entrante est authentifiée.

Ce que ne couvrira pas ce tutoriel

La sécurisation du contenu lorsque l'utilisateur est déconnecté d'internet.

En effet sans serveur pour authentifier une personne, une application hors-ligne ne peut compter que sur son code JavaScript qui peut facilement être cracké par n'importe qui d'un peu tenace.
Nous partirons donc du principe que si quelqu'un possède le site dans son AppCache c'est qu'il s'est au préalable authentifié sur la version en ligne du site.

En revanche si l'utilisateur qui a une version dans l'AppCache est (ou redeviens) connecté à internet, son authentification sera vérifiée au prochain chargement de page.

Implémentation

1. Changer le Cache-Control du manifeste

Un des principes de l'AppCache à appréhender c'est qu'il ne remplace pas le système de mise en cache "classique" des navigateurs. Ainsi si le Cache-Control du manifeste indique qu'il n'est pas expiré, votre navigateur ne tentera pas de le recharger, même si vous êtes connecté, même si ce dernier a changé sur le serveur.

Donc pour que cela fonctionne correctement il faut que vous vous assuriez que le manifeste reviens avec un Cache-Control: no-cache.

Ceci permet de réaliser deux choses, si connecté : d'une part l'application détecte plus rapidement si une mise à jour est disponible mais surtout c'est par ce biais que nous allons pouvoir nous assurer que l'utilisateur est toujours authentifié et ce quelle que soit la page d'entrée de l'utilisateur.

2. Sortir la page d'authentification de l'AppCache

Le manifeste de l'AppCache fonctionne comme une sorte de vases communicants, toutes les urls qui ne sont pas déclarées dans la section Cache doivent l'être dans la section Network. Et si une url se retrouve déclarée nulle part et bien elle est ignorée !

Du coup il est important que votre page d'authentification (et ses dépendances éventuelles) soient déclarées dans la section Network afin que le navigateur aille toujours requêter le serveur. Si vous postez votre formulaire sur une autre url il est d'autant plus crucial que ladite url soit sortie de l'AppCache.

3. Sécuriser le manifeste

L'un des avantages du manifeste dans notre scénario est qu'il est présent sur toutes les pages devant appartenir à l'AppCache ce qui implique qu'il sera vérifié à chaque fois que l'utilisateur naviguera sur l'une de ces pages. Et comme nous venons de définir que le manifeste ne doit pas être mis en cache, il sera donc requêté auprès du serveur à chaque fois.

Du coup ce que vous devez faire c'est utiliser votre d'API d'authentification pour vérifier que l'utilisateur ayant initié la requête pour le manifeste est bien toujours connecté. Si c'est toujours le cas pas de problème renvoyez le contenu du manifeste et sinon renvoyez un code HTTP 401 Unauthorized.

Faite attention à ne pas renvoyer de header Location: autrement l'AppCache risque de ne pas avoir le comportement attendu.

4. Gérer le retour de l'appel au manifeste

L'AppCache dispose d'une API JavaScript permettant notamment de se brancher sur les divers évènements pouvant être émis lors de la vérification du manifeste.

Voici un morceau de code JavaScript permettant de traiter le code retour du manifeste et rediriger vers la page d'authentification si nécessaire à inclure dans toutes les pages incluses dans l'AppCache.

      var manifestUrl = "/manifest.appcache"; // your manifest url
      var loginPageUrl = "/Login.aspx"; // your login page url to redirect if not authorize.
      var isDebug = true; // flag to have debug traces of the AppCache events. Set to false to optimize.
      
      var cacheStatusValues = [];
      cacheStatusValues[0] = 'uncached';
      cacheStatusValues[1] = 'idle';
      cacheStatusValues[2] = 'checking';
      cacheStatusValues[3] = 'downloading';
      cacheStatusValues[4] = 'updateready';
      cacheStatusValues[5] = 'obsolete';

      function handleCacheLog(e) {
        var status, type, message;
        status = cacheStatusValues[appCache.status];
        type = e.type;
        message = 'event: ' + type;
        message += ', status code: ' + appCache.status;
        message += ', status: ' + status;
        console.log(message);
      }

      function GetManifestHttpStatusCode() {
        var xhr = new (window.ActiveXObject || XMLHttpRequest)("Microsoft.XMLHTTP");
        xhr.open("GET", manifestUrl + "?rand=" + Math.floor((1 + Math.random()) * 0x10000), false);
        try {
          xhr.send();
          return xhr.status;
        } catch (error) {
          return 500;
        }
      }

      function handleCacheError(e) {
        if (isDebug)
          handleCacheLog(e);
        // status property is only provided by chrome, use xhr call to get HTTP status code
        var status = (e.status || GetManifestHttpStatusCode())

        // if 401, user is not logged anymore, redirect to login page.
        // if other, user is probably not connected to network
        if (status == 401)
          document.location = loginPageUrl;
      }

      var appCache = window.applicationCache;
      appCache.addEventListener('error', handleCacheError, false);
      if (isDebug) {
        appCache.addEventListener('cached', handleCacheLog, false);
        appCache.addEventListener('checking', handleCacheLog, false);
        appCache.addEventListener('downloading', handleCacheLog, false);
        appCache.addEventListener('noupdate', handleCacheLog, false);
        appCache.addEventListener('obsolete', handleCacheLog, false);
        appCache.addEventListener('progress', handleCacheLog, false);
        appCache.addEventListener('updateready', handleCacheLog, false);
      }

Changez les valeurs des trois variables en entête pour conformer le script à votre application.

Basiquement si une erreur survient lors de la vérification du manifeste alors on teste le code retour HTTP, si c'est un 401 nous ne sommes pas authentifié (ou notre session a expiré) et l'on redirige vers la page d'authentification et dans les autres cas (incluant le fait que le device utilisateur soit hors-ligne) on ignore l'erreur.
Et bien sûr le cas où tout va bien et qui n'a pas besoin de gestion JavaScript particulière : nous sommes authentifié, le navigateur récupère correctement le manifeste et décide, ou pas, de monter l'application dans l'AppCache.

En regardant plus en détail mon implémentation vous pourrez voir que j'ai dû palier à quelques différences d'implémentation entre les navigateurs. Google Chrome est le plus aidant car il retourne directement le code retour dans la propriété status de l'argument e, pour les autres navigateurs un appel AJAX au manifeste pour récupérer le code s'impose.

Et là une bizarrerie, la même requête qui avait échouée auparavant revenait avec un code 200 presque comme si elle n'avait pas été exécutée ou bien tirée du cache. C'est pourquoi j'ai rajouté un chiffre aléatoire dans la query string de l'appel au manifeste pour forcer le navigateur à relancer la requête, et revenir cette fois-ci avec le résultat espéré.

SAUF QUE, en rajoutant ce paramètre nous avons changé l'url du point de vue du manifeste ce qui contraint donc à effectuer le point suivant.

5. Autoriser toutes les urls non-cachées à être exécutées dans le manifeste

Afin de pouvoir laisser passer l'appel à l'url du manifeste avec un paramètre aléatoire nous allons avoir besoin de rajouter le métacaractère * dans la section Network.

Attention ceci n'a pas pour effet de sortir toutes les urls de l'AppCache cela indique simplement que toutes les urls qui ne sont pas déclarées dans la section Cache sont autorisées à contacter le serveur.

Conclusion

Voila avec ce procédé vous pourrez gérer à la fois une application nécessitant une mise en cache via le Offline Application Cache tout en continuant à assurer la sécurisation du contenu (du moins temps que le navigateur reste en ligne).

Laissez un commentaire

3 Commentaires

  • Tu as un exemple de manifeste à montrer ?

  • Si tu veux mais chaque manifeste est propre à son application, la seule "contrainte" que je demande ici c'est que le wildcard soit déclaré dans la section Network, que la page de Login ne soit pas dans la section cache et que le manifeste reviennes avec une entête HTTP précisant no-cache.

    Spoiler (Sélectionnez le texte dans le cadre pointillé pour le faire apparaître)

    CACHE MANIFEST
    # 2014-11-20 12:34:12Z
    
    CACHE:
    /Css/bootstrap.css
    /Css/non-responsive.css
    /Css/style.css
    /fonts/fontawesome-webfont.eot
    /fonts/fontawesome-webfont.svg
    /fonts/fontawesome-webfont.ttf
    /fonts/fontawesome-webfont.woff
    /fonts/glyphicons-halflings-regular.eot
    /fonts/glyphicons-halflings-regular.svg
    /fonts/glyphicons-halflings-regular.ttf
    /fonts/glyphicons-halflings-regular.woff
    /fonts/Gotham-Bold.eot
    /fonts/Gotham-Bold.otf
    /fonts/Gotham-Bold.svg
    /fonts/Gotham-Bold.woff
    /fonts/Gotham-BoldIta.eot
    /fonts/Gotham-BoldIta.otf
    /fonts/Gotham-BoldIta.svg
    /fonts/Gotham-BoldIta.woff
    /fonts/Gotham-Book.eot
    /fonts/Gotham-Book.otf
    /fonts/Gotham-Book.svg
    /fonts/Gotham-Book.woff
    /fonts/Gotham-Medium.eot
    /fonts/Gotham-Medium.otf
    /fonts/Gotham-Medium.svg
    /fonts/Gotham-Medium.woff
    /images/apple-touch-icon-114x114.png
    /images/apple-touch-icon-144x144.png
    /images/apple-touch-icon-57x57.png
    /images/apple-touch-icon-72x72.png
    /images/bgd-repeat-x-1.png
    /images/bgd-repeat-x.png
    /images/bottle.png
    /images/favicon.ico
    /images/icon-back.png
    /images/icon-home.png
    /images/icon-next.png
    /images/logo-cocacola.png
    /Scripts/bootstrap.js
    /Scripts/l10n.js
    /Scripts/libs.js
    /Scripts/lunr.min.js
    /Scripts/modernizr.js
    /Scripts/plugins.js
    /Scripts/responsive.js
    /Scripts/start.js
    /media/1001/logo-1.jpg?width=37&height=40
    /
    /mentions-legales/
    
    NETWORK:
    /umbraco_client/
    /documents.json
    /PipelineQueueExecution.json
    *
    

  • C'est quand même beaucoup plus clair avec un exemple de manifeste, merci !

Laissez un commentaire

Vous devez être connecté pour commenter sur le Refuge. Identifiez-vous maintenant ou inscrivez-vous !


Marre des pubs ? Inscrivez-vous !