web-dev-qa-db-fra.com

Application d'une seule page avec authentification basée sur les cookies HttpOnly et gestion de session

Depuis plusieurs jours, je recherche un mécanisme sécurisé d'authentification et de gestion de session pour mon application monopage. À en juger par les nombreux tutoriels et articles de blog sur les SPA et l'authentification, le stockage des JWT dans localStorage ou des cookies réguliers semble être l'approche la plus courante, mais ce n'est tout simplement pas une bonne idée d'utiliser des JWT pour les sessions donc ce n'est pas une option pour cette application.

Exigences

  • Les identifiants doivent être révocables. Par exemple, si une activité suspecte est détectée sur le compte d'un utilisateur, il devrait être possible de révoquer immédiatement l'accès de cet utilisateur et d'exiger une nouvelle connexion. De même, des choses comme la réinitialisation du mot de passe doivent révoquer toutes les connexions existantes.
  • L'authentification devrait avoir lieu sur Ajax. Aucune redirection de navigateur (redirection "matérielle") ne devrait avoir lieu par la suite. Toute navigation doit se faire à l'intérieur du SPA.
  • Tout devrait fonctionner entre domaines. Le SPA sera sur www.domain1.com et le serveur sera sur www.domain2.com.
  • JavaScript ne doit pas avoir accès aux données sensibles envoyées par le serveur, telles que les ID de session. Il s'agit d'empêcher les attaques XSS où des scripts malveillants peuvent voler les jetons ou les ID de session des cookies réguliers, localStorage ou sessionStorage.

Le mécanisme idéal semble être l'authentification basée sur les cookies utilisant des cookies HttpOnly qui contiennent des ID de session. Le flux fonctionnerait comme ceci:

  1. L'utilisateur arrive sur une page de connexion et soumet son nom d'utilisateur et son mot de passe.
  2. Le serveur authentifie l'utilisateur et envoie un ID de session en tant que cookie de réponse HttpOnly.
  3. Le SPA inclut ensuite ce cookie dans les demandes XHR suivantes adressées au serveur. Cela semble possible en utilisant l'option withCredentials.
  4. Lorsqu'une demande est adressée à un point de terminaison protégé, le serveur recherche le cookie. S'il est trouvé, il vérifie cet ID de session par rapport à une table de base de données pour s'assurer que la session est toujours valide.
  5. Lorsque l'utilisateur se déconnecte, la session est supprimée de la base de données. La prochaine fois que l'utilisateur arrive sur le site, le SPA obtient une réponse 401/403 (depuis la fin de la session), puis amène l'utilisateur à l'écran de connexion.

Parce que le cookie a l'indicateur HttpOnly, JavaScript ne pourrait pas lire son contenu, il serait donc à l'abri des attaques tant qu'il est transmis via HTTPS.

Défis

Voici le problème spécifique que j'ai rencontré. Mon serveur est configuré pour gérer les demandes CORS. Après authentification de l'utilisateur, il envoie correctement le cookie dans la réponse:

HTTP/1.1 200 OK
server: Cowboy
date: Wed, 15 Mar 2017 22:35:46 GMT
content-length: 59
set-cookie: _myapp_key=SFMyNTYBbQAAABBn; path=/; HttpOnly
content-type: application/json; charset=utf-8
cache-control: max-age=0, private, must-revalidate
x-request-id: qi2q2rtt7mpi9u9c703tp7idmfg4qs6o
access-control-allow-Origin: http://localhost:8080
access-control-expose-headers: 
access-control-allow-credentials: true
vary: Origin

Cependant, le navigateur n'enregistre pas le cookie (lorsque je vérifie les cookies locaux de Chrome, il n'y est pas). Ainsi, lorsque le code suivant s'exécute:

context.axios.post(LOGIN_URL, creds).then(response => {
  context.$router.Push("/api/account")
}

Et la page Compte est créée:

created() {
  this.axios.get(SERVER_URL + "/api/account/", {withCredentials: true}).then(response => {
    //do stuff
  }
}

Cet appel n'a pas le cookie dans l'en-tête. Le serveur la rejette donc.

GET /api/account/ HTTP/1.1
Host: localhost:4000
Connection: keep-alive
Accept: application/json
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Referer: http://localhost:8080/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8,tr;q=0.6

Dois-je faire quelque chose de spécial pour m'assurer que le navigateur enregistre le cookie après l'avoir reçu dans la réponse de connexion? Diverses sources que j'ai lues ont dit que les navigateurs ne sauvegardent les cookies de réponse que lorsque l'utilisateur est redirigé, mais je ne veux pas de redirections "dures" car il s'agit d'un SPA.

Le SPA en question est écrit dans Vue.js, mais je suppose qu'il s'applique à tous les SPA. Je me demande comment les gens gèrent ce scénario.

D'autres choses que j'ai lues sur ce sujet:

36
Ege Ersoz

J'ai obtenu ce travail sur Vue.js 2 avec credentials = true. La définition des informations d'identification du site client ne représente que la moitié de l'histoire. Vous devez également définir des en-têtes de réponse à partir du serveur:

    header("Access-Control-Allow-Origin: http://localhost:8080");
    header("Access-Control-Allow-Credentials: true");

Vous ne pouvez pas utiliser de caractères génériques pour Access-Control-Allow-Origin comme ceci:

header("Access-Control-Allow-Origin: *");

Lorsque vous spécifiez les informations d'identification: en-tête vrai, vous devez spécifier l'origine.

Comme vous pouvez le voir, il s'agit d'un code PHP, vous pouvez le modéliser vers NodeJS ou tout autre langage de script côté serveur que vous utilisez.

Dans VueJS, je définis credentials = true comme ceci dans le fichier main.js:

Vue.http.options.credentials = true

Dans le composant, je me suis connecté avec succès en utilisant ajax:

<template>
<div id="userprofile">
    <h2>User profile</h2>
    {{init()}}

</div>
</template>

<script>
    export default {  
        name: 'UserProfile',
        methods: {
        init: function() {


                // loggin in
              console.log('Attempting to login');

            this.$http.get('https://localhost/api/login.php')
            .then(resource =>  {
             // logging response - Notice I don't send any user or password for testing purposes  

            });

            // check the user profile

                console.log('Getting user profile');

                this.$http.get('https://localhost/api/userprofile.php')
                .then(resource =>  {
                    console.log(resource.data);
                })

        }
    }
    }

</script>

Côté serveur, les choses sont assez simples: Login.php sur définit un cookie sans effectuer de validation (Remarque: ceci est effectué à des fins de test uniquement. Il n'est pas conseillé d'utiliser ce code en production sans validation)

<?php

header("Access-Control-Allow-Origin: http://localhost:8080");
header("Access-Control-Allow-Credentials: true");

$cookie = setcookie('user', 'student', time()+3600, '/', 'localhost', false , true);

if($cookie){
  echo "Logged in";
}else{
  echo "Can't set a cookie";
}

Enfin, userprofile.php vérifie simplement si un cookie = user est défini

<?php

  header("Access-Control-Allow-Origin: http://localhost:8080");
    header("Access-Control-Allow-Credentials: true");


if(isset($_COOKIE['user'])){
  echo "Congratulations the user is ". $_COOKIE['user'];

}else{
  echo "Not allowed to access";
}

Connexion réussie

Successfully logged in

3
Programmer

Avec les paramètres de contrôle des informations d'identification et l'envoi de cookies, assurez-vous de l'activer lors de la publication pour vous connecter afin que le cookie renvoyé soit enregistré dans le navigateur.

0
RDelorier