Node.js

Autenticació basada en sessions

  

Com ja comentem, l’autenticació basada en sessions és un dels mecanismes més utilitzats en aplicacions web tradicionals, especialment en aquelles executades en navegadors. Este enfocament permet al servidor identificar a un usuari autenticat i recordar-lo al llarg de la seua interacció amb l’aplicació.

En este document, aprendrem com funcionen les sessions, com configurar-les en Express i com definir mecanismes d’autenticació i validació d’usuaris.

1. Fonaments de l’autenticació basada en sessions

L’autenticació basada en sessions resol una limitació del protocol HTTP, el qual és sense estat. Això significa que cada petició que el client envia al servidor és independent i no guarda informació sobre peticions anteriors. Sense un mecanisme addicional, el servidor no pot “recordar” qui està interactuant amb ell.

Per a solucionar això, s’introduïxen les sessions:

1.1 Esquema de funcionament de l’autenticació basada en sessions

  1. Inici de sessió (login):
    • L’usuari accedix a una zona restringida de l’aplicació i proporciona les seues credencials (per exemple, usuari i contrasenya).
    • El servidor verifica les credencials. Si són correctes:
    • Es crea una nova sessió per a eixe usuari.
    • El servidor emmagatzema en la sessió informació rellevant, com el nom d’usuari, el seu rol (administrador, editor, visitant, etc.), o qualsevol altra dada necessària. * El servidor envia al client un ID de sessió (generalment en forma de cookie).
  2. Petició de recursos autenticats:
    • En cada petició posterior al servidor, el client envia automàticament l’ID de sessió (a través de la cookie).
    • El servidor rep l’ID, recupera la informació de sessió emmagatzemada, i “recorda” a l’usuari que està interactuant.
    • En funció de les dades de la sessió (ex. rol de l’usuari), el servidor decidix si permet o denega l’accés a unes certes accions o recursos.
  3. Tancament de sessió (logout):
    • Quan l’usuari decidix tancar sessió, el servidor elimina les dades de la sessió.
    • La cookie de sessió en el client queda invalidada, i el servidor ja no pot identificar a l’usuari.

Si les cookies no estan permeses, la solució més comuna en aplicacions tradicionals és enviar l’ID de sessió en la URL com a paràmetre (query string) o incloure-ho en el cos de les peticions. No obstant això, totes dues opcions poden comprometre la seguretat si no s’implementen correctament. Per a aplicacions modernes o APIs, la millor alternativa és migrar a autenticació basada en tokens (com JWT), ja que no depenen de cookies i són més segures i escalables.

2. Definició de sessions en Express

Per a poder treballar amb sessions en Express instal·larem el mòdul express-session. És un middleware que permet, en cada petició que requerisca una comprovació, determinar si l’usuari ja s’ha validat i amb quines credencials, abans de deixar-li accedir al que cerca o no.

Així que el primer que farem serà instal·lar el mòdul:

npm install express-session

Després, ho incorporem al nostre servidor Express juntament amb la resta de mòduls:

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

A continuació, configurem la sessió dins de l’aplicació Express:

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

Els paràmetres de configuració que hem emprat són:

Existeixen altres paràmetres i opcions de configuració, tal com podem consultar en la web del repositori NPM.

NOTA: la configuració de la sessió haurà de fer-se ABANS de definir els encaminadors, ja que en cas contrari aquest middleware s’aplicarà després de processar les rutes, i no tindrà efecte.

2.1. Validació

En tot procés d’autenticació ha d’haver-hi una validació prèvia, on l’usuari envie les seues credencials i s’acaren amb les existents en la base de dades, abans de deixar-li accedir.

Suposarem, per simplicitat, que tenim els usuaris carregats en un array, amb el seu nom d’usuari i el seu password:

const usuaris = [
    { usuari: 'nacho', password: '12345' },
    { usuari: 'pepe', password: 'pepe111' }
];

Ara hauríem de definir una ruta que, normalment per POST, recollira les credencials que envia l’usuari i les acarara amb aqueix array. Si concorda amb algun usuari emmagatzemat, es guarda en la sessió el nom de l’usuari que va accedir al sistema, i es pot redirigir a alguna pàgina d’inici. En cas contrari, es pot redirigir a una pàgina de login:

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

    let existeixUsuari = usuaris.filter(usuari => 
    usuari.usuari == login && usuari.password == password);

    if (existeixUsuari.length > 0)
    {
        req.session.usuari = existeUsuario[0].usuari;
        res.render('index');
    } else {
        res.render('login', 
            {error: "Usuari o contrasenya incorrectes"});
    }
});

2.2. Autenticació

Una vegada validat l’usuari, hem de definir una funció middleware que s’encarregarà d’aplicar-se en cada ruta que vulguem protegir. El que farà serà comprovar si hi ha algun usuari en sessió. En cas afirmatiu, deixarà passar la petició. En cas contrari, enviarà a la pàgina de validació o login, per exemple.

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

Només ens queda aplicar aquest middleware en cada ruta que requerisca validació per part de l’usuari. Això es fa en la mateixa anomenada a get, post, put o delete:

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

Notar que passem com a segon paràmetre el middleware d’autenticació. Si passa aqueix filtre, s’executarà el codi del get. En cas contrari, el middleware està configurat per a renderitzar la vista de login.

3. Definint rols

La nostra aplicació també pot tindre diferents rols per als usuaris registrats. Per exemple, podem tindre administradors i usuaris normals. Això se sol definir amb un camp extra en la informació dels usuaris:

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

Quan un usuari valide les seues credencials, a més d’emmagatzemar el seu nom d’usuari en sessió, també podem (devem) emmagatzemar el seu rol. Així que la ruta que valida l’usuari es veu modificada per a afegir aquesta nova dada en sessió:

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

    ...

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

Per a poder comprovar si un usuari validat té el rol adequat per a accedir a un recurs, podem definir una altra funció middleware que comprove si el rol de l’usuari és el que es necessita (el que se li passa com a paràmetre a la funció):

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

NOTA: l’exemple que acabem de veure és una mostra de com podem definir middleware que necessite paràmetres addicionals a més dels tres que tot middleware ha de tindre (petició, resposta i següent funció a cridar). N’hi ha prou amb definir una funció amb els paràmetres necessaris, i que internament retorne la funció middleware amb els tres paràmetres base.

Si volem aplicar els dos middleware a una ruta determinada (és a dir, comprovar si l’usuari està autenticat i, a més, si té el rol adequat), podem passar-los un darrere l’altre, separats per comes, en la definició de la ruta. Per exemple, a aquesta ruta només han de poder accedir usuaris validats que tinguen rol d’administrador:

app.get('/protegitAdmin', autenticacio, 
    rol('admin'), (req, res) => {
    res.render('protegit_admin');
})

4. Altres opcions

A més de les opcions vistes anteriorment, hi ha algunes operacions més que, si bé poden ser secundàries, convé tindre presents quan treballem amb autenticació basada en sessions.

4.1. Tancament de sessió o logout

D’una banda, està la possibilitat de fer logout i eixir de la sessió. Per a això, podem definir una ruta que responga a aquesta petició, i destruïsca les dades de sessió de l’usuari, redirigint després a un altre recurs:

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

4.2. Accedir a la sessió des de les vistes

Per a fer que la informació de la sessió estiga accessible des de les vistes, hem de definir un middleware que copie el contingut de *req.session a cap de res.locals.session.

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

NOTA: aquest middleware ha de definir-se després del middleware que configura la sessió i abans dels encaminadors, perquè tinga efecte en renderitzar les vistes.

Després, podem accedir a aquesta sessió des de les vistes, a través de la variable session que hem definit en la resposta (res.locals). Per exemple, així podríem veure si un usuari està ja logueado, per a mostrar o no el botó de “Login”:

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

4.3. Temps de vida de la sessió

A més, podem establir el temps de vida de la sessió, quan la configurem. Podem fer-ho utilitzant indistintament l’atribut expires o l’atribut maxAge, encara que amb una sintaxi una mica diferent segons quin utilitzem. Hem d’indicar el nombre de mil·lisegons de vida, comptant des del moment actual, per la qual cosa se sol utilitzar Date.now() en aquests càlculs. Així definiríem, per exemple, una sessió de 30 minuts:

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

Ací pots descarregar un exemple complet per a provar aquests mecanismes. Es té una pàgina d’inici pública, una restringida per a usuaris validats i una altra restringida per a usuaris administradors. Es disposa també d’un formulari de login i d’una ruta de logout.

Exercici 1:

Crea una còpia de l’exercici LlibresWeb_v4 i crida-la LlibresWebSessions. A partir d’aqueixa base, afegirem ara autenticació basada en sessions. Instal·la el middleware express-session en el projecte, i configura’l com en l’exemple vist abans. Defineix a mà en el servidor principal un array amb noms i passwords d’usuaris autoritzats, i protegeix les rutes que permeten fer qualsevol modificació sobre el catàleg de llibres. En concret, només els usuaris validats podran:

Afig per a això una vista login.njk al conjunt de vistes de l’aplicació. Pots emprar el mateix formulari de login que en l’exemple, i també afig les dues rutes per a mostrar el formulari i per a recollir les dades i validar l’usuari. En cas de validació reeixida, es renderitzarà la vista del llistat de llibres. En cas contrari, el formulari de login amb un missatge d’error, com en l’exemple proporcionat.

Afig també una funció de logout al menú de l’aplicació, que només serà visible si l’usuari ja està validat, i que permetrà destruir la seua sessió i redirigir al llistat de llibres.