reactjs - from - Aplicación de una sola página con autenticación basada en cookies HttpOnly y administración de sesiones
react cookies (2)
Desde hace varios días he estado buscando un mecanismo seguro de autenticación y administración de sesión para mi aplicación de una sola página. A juzgar por los numerosos tutoriales y publicaciones de blogs sobre SPA y autenticación, el almacenamiento de JWT en almacenamiento local o cookies regulares parece ser el enfoque más común, pero simplemente no es una buena idea usar JWT para las sesiones, por lo que no es una opción para esta aplicación. .
Requerimientos
- Los inicios de sesión deben ser revocables. Por ejemplo, si se detecta actividad sospechosa en la cuenta de un usuario, debería ser posible revocar el acceso de ese usuario inmediatamente y requerir un nuevo inicio de sesión. Del mismo modo, cosas como el restablecimiento de contraseñas deben revocar todos los inicios de sesión existentes.
- La autenticación debe ocurrir sobre Ajax. No se debe realizar una redirección del navegador (redirección "dura") después. Toda la navegación debe pasar dentro del SPA.
- Todo debería funcionar de dominio cruzado. El SPA estará en www.domain1.com y el servidor en www.domain2.com.
- JavaScript no debe tener acceso a datos confidenciales enviados por el servidor, como los ID de sesión. Esto es para evitar ataques XSS donde las secuencias de comandos malintencionadas pueden robar los tokens o las ID de sesión de las cookies regulares, el almacenamiento local o el almacenamiento de sesión.
El mecanismo ideal parece ser la autenticación basada en cookies utilizando cookies HttpOnly
que contienen identificadores de sesión. El flujo funcionaría así:
- El usuario llega a una página de inicio de sesión y envía su nombre de usuario y contraseña.
- El servidor autentica al usuario y envía un ID de sesión como una cookie de respuesta
HttpOnly
. - El SPA luego incluye esta cookie en las siguientes solicitudes de XHR hechas al servidor. Esto parece ser posible usando la opción
withCredentials
. - Cuando se realiza una solicitud a un punto final protegido, el servidor busca la cookie. Si lo encuentra, verifica ese ID de sesión contra una tabla de base de datos para asegurarse de que la sesión sigue siendo válida.
- Cuando el usuario cierra la sesión, la sesión se elimina de la base de datos. La próxima vez que el usuario llega al sitio, el SPA recibe una respuesta 401/403 (ya que la sesión ha expirado), luego lleva al usuario a la pantalla de inicio de sesión.
Debido a que la cookie tiene la HttpOnly
, JavaScript no podrá leer su contenido, por lo que estaría a salvo de los ataques siempre que se transmita a través de HTTPS.
Desafíos
Aquí está el problema específico que he encontrado. Mi servidor está configurado para manejar solicitudes CORS. Después de autenticar al usuario, envía correctamente la cookie en la respuesta:
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
Sin embargo, el navegador no guarda la cookie (cuando verifico las cookies locales de Chrome no están allí). Entonces cuando se ejecuta el siguiente código:
context.axios.post(LOGIN_URL, creds).then(response => {
context.$router.push("/api/account")
}
Y se crea la página de la cuenta:
created() {
this.axios.get(SERVER_URL + "/api/account/", {withCredentials: true}).then(response => {
//do stuff
}
}
Esta llamada no tiene la cookie en el encabezado. El servidor por lo tanto lo rechaza.
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
¿Debo hacer algo especial para asegurarme de que el navegador guarde la cookie después de recibirla en la respuesta de inicio de sesión? Varias fuentes que he leído dicen que los navegadores guardan las cookies de respuesta solo cuando el usuario es redirigido, pero no quiero redireccionamientos "duros" ya que esto es un SPA.
El SPA en cuestión está escrito en Vue.js, pero supongo que se aplica a todos los SPA. Me pregunto cómo la gente maneja este escenario.
Otras cosas que he leído sobre este tema:
Con los controles de configuración de credenciales y el envío de cookies, asegúrese de activarlo al publicar para iniciar sesión para que la cookie devuelta se guarde en el navegador.
Conseguí esto trabajando en Vue.js 2 con credenciales = true. Configuración de credenciales desde el sitio del cliente sólo la mitad de la historia. También debe configurar los encabezados de respuesta desde el servidor:
header("Access-Control-Allow-Origin: http://localhost:8080");
header("Access-Control-Allow-Credentials: true");
No puedes usar comodines para Access-Control-Allow-Origin de esta manera:
header("Access-Control-Allow-Origin: *");
Cuando especifique las credenciales: verdadero encabezado, se le solicitará que especifique el origen.
Como puede ver, este es un código PHP, puede modelarlo para NodeJS o cualquier lenguaje de script del lado del servidor que esté utilizando.
En VueJS establecí credenciales = verdaderas así en main.js:
Vue.http.options.credentials = true
En el componente, inicié sesión correctamente usando 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>
En el lado del servidor, las cosas son bastante simples: Login.php on establece una cookie sin realizar ninguna validación (Nota: esto se hace solo con fines de prueba. No se recomienda utilizar este código en producción sin validación)
<?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";
}
Finalmente, userprofile.php solo verifica si una cookie = usuario está configurada
<?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";
}
Ha iniciado sesión correctamente