Node.js

Autenticación basada en sesiones

     

Como ya comentamos, la autenticación basada en sesiones es uno de los mecanismos más utilizados en aplicaciones web tradicionales, especialmente en aquellas ejecutadas en navegadores. Este enfoque permite al servidor identificar a un usuario autenticado y recordarlo a lo largo de su interacción con la aplicación.

En este documento, aprenderemos cómo funcionan las sesiones, cómo configurarlas en Express y cómo definir mecanismos de autenticación y validación de usuarios.

1. Fundamentos de la autenticación basada en sesiones

La autenticación basada en sesiones resuelve una limitación del protocolo HTTP, el cual es sin estado. Esto significa que cada petición que el cliente envía al servidor es independiente y no guarda información sobre peticiones anteriores. Sin un mecanismo adicional, el servidor no puede “recordar” quién está interactuando con él.

Para solucionar esto, se introducen las sesiones:

1.1 Esquema de funcionamiento de la autenticación basada en sesiones

  1. Inicio de sesión (login):
    • El usuario accede a una zona restringida de la aplicación y proporciona sus credenciales (por ejemplo, usuario y contraseña).
    • El servidor verifica las credenciales. Si son correctas:
      • Se crea una nueva sesión para ese usuario.
      • El servidor almacena en la sesión información relevante, como el nombre de usuario, su rol (administrador, editor, visitante, etc.), o cualquier otro dato necesario.
      • El servidor envía al cliente un ID de sesión (generalmente en forma de cookie).
  2. Petición de recursos autenticados:
    • En cada petición posterior al servidor, el cliente envía automáticamente el ID de sesión (a través de la cookie).
    • El servidor recibe el ID, recupera la información de sesión almacenada, y “recuerda” al usuario que está interactuando.
    • En función de los datos de la sesión (ej. rol del usuario), el servidor decide si permite o deniega el acceso a ciertas acciones o recursos.
  3. Cierre de sesión (logout):
    • Cuando el usuario decide cerrar sesión, el servidor elimina los datos de la sesión.
    • La cookie de sesión en el cliente queda invalidada, y el servidor ya no puede identificar al usuario.

Si las cookies no están permitidas, la solución más común en aplicaciones tradicionales es enviar el ID de sesión en la URL como parámetro (query string) o incluirlo en el cuerpo de las peticiones. Sin embargo, ambas opciones pueden comprometer la seguridad si no se implementan correctamente. Para aplicaciones modernas o APIs, la mejor alternativa es migrar a autenticación basada en tokens (como JWT), ya que no dependen de cookies y son más seguras y escalables.

2. Definición de sesiones en Express

Para poder trabajar con sesiones en Express vamos a instalar el módulo express-session. Es un middleware que permite, en cada petición que requiera una comprobación, determinar si el usuario ya se ha validado y con qué credenciales, antes de dejarle acceder a lo que busca o no.

Así que lo primero que haremos será instalar el módulo:

npm install express-session

Después, lo incorporamos a nuestro servidor Express junto con el resto de módulos:

const express = require('express');
const session = require('express-session');
...

A continuación, configuramos la sesión dentro de la aplicación Express:

let app = express();
...
app.use(session({
    secret: '1234',
    resave: true,
    saveUninitialized: false
}));

Los parámetros de configuración que hemos empleado son:

Existen otros parámetros y opciones de configuración, tal y como podemos consultar en la web del repositorio NPM.

NOTA: la configuración de la sesión deberá hacerse ANTES de definir los enrutadores, ya que de lo contrario este middleware se aplicará después de procesar las rutas, y no tendrá efecto.

2.1. Validación

En todo proceso de autenticación debe haber una validación previa, donde el usuario envíe sus credenciales y se cotejen con las existentes en la base de datos, antes de dejarle acceder.

Vamos a suponer, por simplicidad, que tenemos los usuarios cargados en un array, con su nombre de usuario y su password:

const usuarios = [
    { usuario: 'nacho', password: '12345' },
    { usuario: 'pepe', password: 'pepe111' }
];

Ahora tendríamos que definir una ruta que, normalmente por POST, recogiera las credenciales que envía el usuario y las cotejara con ese array. Si concuerda con algún usuario almacenado, se guarda en la sesión el nombre del usuario que accedió al sistema, y se puede redirigir a alguna página de inicio. En caso contrario, se puede redirigir a una página de login:

app.post('/login', (req, res) => {
    let login = req.body.login;
    let password = req.body.password;

    let existeUsuario = usuarios.filter(usuario => 
        usuario.usuario == login && usuario.password == password);

    if (existeUsuario.length > 0)
    {
        req.session.usuario = existeUsuario[0].usuario;
        res.render('index');
    } else {
        res.render('login', 
                   {error: "Usuario o contraseña incorrectos"});
    }
});

2.2. Autenticación

Una vez validado el usuario, debemos definir una función middleware que se encargará de aplicarse en cada ruta que queramos proteger. Lo que hará será comprobar si hay algún usuario en sesión. En caso afirmativo, dejará pasar la petición. De lo contrario, enviará a la página de validación o login, por ejemplo.

let autenticacion = (req, res, next) => {
    if (req.session && req.session.usuario)
        return next();
    else
        res.render('login');
};

Sólo nos queda aplicar este middleware en cada ruta que requiera validación por parte del usuario. Esto se hace en la misma llamada a get, post, put o delete:

app.get('/protegido', autenticacion, (req, res) => {
    res.render('protegido');
});

Notar que pasamos como segundo parámetro el middleware de autenticación. Si pasa ese filtro, se ejecutará el código del get. En caso contrario, el middleware está configurado para renderizar la vista de login.

3. Definiendo roles

Nuestra aplicación también puede tener distintos roles para los usuarios registrados. Por ejemplo, podemos tener administradores y usuarios normales. Esto se suele definir con un campo extra en la información de los usuarios:

const usuarios = [
    { usuario: 'nacho', password: '12345', rol: 'admin' },
    { usuario: 'pepe', password: 'pepe111', rol: 'normal' }
];

Cuando un usuario valide sus credenciales, además de almacenar su nombre de usuario en sesión, también podemos (debemos) almacenar su rol. Así que la ruta que valida el usuario se ve modificada para añadir este nuevo dato en sesión:

app.post('/login', (req, res) => {
    let login = req.body.login;
    let password = req.body.password;

    ...

    if (existeUsuario.length > 0)
    {
        req.session.usuario = existeUsuario[0].usuario;
        req.session.rol = existeUsuario[0].rol;
        res.render('index');
    } else {
        ...
    }
});

Para poder comprobar si un usuario validado tiene el rol adecuado para acceder a un recurso, podemos definir otra función middleware que compruebe si el rol del usuario es el que se necesita (el que se le pasa como parámetro a la función):

let rol = (rol) => {
    return (req, res, next) => {
        if (rol === req.session.rol)
            next();
        else
            res.render('login');
    }
}

NOTA: el ejemplo que acabamos de ver es una muestra de cómo podemos definir middleware que necesite parámetros adicionales además de los tres que todo middleware debe tener (petición, respuesta y siguiente función a llamar). Basta con definir una función con los parámetros necesarios, y que internamente devuelva la función middleware con los tres parámetros base.

Si queremos aplicar los dos middleware a una ruta determinada (es decir, comprobar si el usuario está autenticado y, además, si tiene el rol adecuado), podemos pasarlos uno tras otro, separados por comas, en la definición de la ruta. Por ejemplo, a esta ruta sólo deben poder acceder usuarios validados que tengan rol de administrador:

app.get('/protegidoAdmin', autenticacion, 
        rol('admin'), (req, res) => {
    res.render('protegido_admin');
})

4. Otras opciones

Además de las opciones vistas anteriormente, hay algunas operaciones más que, si bien pueden ser secundarias, conviene tener presentes cuando trabajamos con autenticación basada en sesiones.

4.1. Cierre de sesión o logout

Por un lado, está la posibilidad de hacer logout y salir de la sesión. Para esto, podemos definir una ruta que responda a esta petición, y destruya los datos de sesión del usuario, redirigiendo después a otro recurso:

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect('/');
});

4.2. Acceder a la sesión desde las vistas

Para hacer que la información de la sesión esté accesible desde las vistas, tenemos que definir un middleware que copie el contenido de req.session a res.locals.session.

app.use((req, res, next) => {
    res.locals.session = req.session;
    next();
});

NOTA: este middleware debe definirse después del middleware que configura la sesión y antes de los enrutadores, para que tenga efecto al renderizar las vistas.

Después, podemos acceder a esta sesión desde las vistas, a través de la variable session que hemos definido en la respuesta (res.locals). Por ejemplo, así podríamos ver si un usuario está ya logueado, para mostrar o no el botón de “Login”:

{% if (session and session.usuario) %}
    <a class="btn btn-dark" href="/logout">Logout</a>
{% else %}
    <a class="btn btn-dark" href="/login">Login</a>
{% endif %}

4.3. Tiempo de vida de la sesión

Además, podemos establecer el tiempo de vida de la sesión, cuando la configuramos. Podemos hacerlo utilizando indistintamente el atributo expires o el atributo maxAge, aunque con una sintaxis algo distinta según cuál utilicemos. Debemos indicar el número de milisegundos de vida, contando desde el momento actual, por lo que se suele utilizar Date.now() en estos cálculos. Así definiríamos, por ejemplo, una sesión de 30 minutos:

app.use(session({
    secret: '1234',
    resave: true,
    saveUninitialized: false,
    expires: new Date(Date.now() + (30 * 60 * 1000))
}));

Aquí puedes descargar un ejemplo completo para probar estos mecanismos. Se tiene una página de inicio pública, una restringida para usuarios validados y otra restringida para usuarios administradores. Se dispone también de un formulario de login y de una ruta de logout.

Ejercicio 1:

Crea una copia del ejercicio LibrosWeb_v4 y llámala LibrosWebSesiones. A partir de esa base, vamos a añadir ahora autenticación basada en sesiones. Instala el middleware express-session en el proyecto, y configúralo como en el ejemplo visto antes. Define a mano en el servidor principal un array con nombres y passwords de usuarios autorizados, y protege las rutas que permitan hacer cualquier modificación sobre el catálogo de libros. En concreto, sólo los usuarios validados podrán:

Añade para ello una vista login.njk al conjunto de vistas de la aplicación. Puedes emplear el mismo formulario de login que en el ejemplo, y también añade las dos rutas para mostrar el formulario y para recoger los datos y validar el usuario. En caso de validación exitosa, se renderizará la vista del listado de libros. En caso contrario, el formulario de login con un mensaje de error, como en el ejemplo proporcionado.

Añade también una función de logout al menú de la aplicación, que sólo será visible si el usuario ya está validado, y que permitirá destruir su sesión y redirigir al listado de libros.