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
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:
HttpOnly
.withCredentials
.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:
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
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.