Manejo de la autenticación en Express.js

La autenticación en muchos casos permite a los usuarios realizar personalmente acciones en un sitio web. En este artículo, manejaremos la autenticación en Node con Express.js.

Introducción

En este artículo, crearemos una aplicación simple para demostrar cómo puede manejar la autenticación en Express.js. Dado que usaremos algunas sintaxis básicas de ES6 y el marco Bootstrap para el diseño de la interfaz de usuario, podría ser útil si tiene algunos conocimientos básicos sobre esas tecnologías.

Aunque es posible que necesite usar una base de datos en una aplicación del mundo real, dado que debemos mantener este artículo simple, no usaremos ninguna base de datos ni métodos de validación de correo electrónico, como enviar un correo electrónico con un código de validación.

Configuración del proyecto

Primero, creemos una nueva carpeta llamada, digamos, simple-web-app. Usando la terminal, navegaremos a esa carpeta y crearemos un proyecto de esqueleto de Node.js:

1
$ npm init

Ahora, también podemos instalar Express:

1
$ npm install --save express

Para simplificar las cosas, usaremos un motor de renderizado del lado del servidor llamado Bigote daliniano. Este motor representará nuestras páginas HTML en el lado del servidor, por lo que no necesitaremos ningún otro marco front-end como Angular o React.

Avancemos e instalemos express-handlebars:

1
$ npm install --save express-handlebars

También usaremos otros dos paquetes de middleware Express (body-parser y cookie-parser) para analizar los cuerpos de solicitud HTTP y analizar las cookies requeridas para la autenticación:

1
$ npm install --save body-parser cookie-parser

Implementación

La aplicación que vamos a construir contendrá una página "protegida" que solo los usuarios registrados pueden visitar; de lo contrario, serán redirigidos a la página de inicio, lo que les pedirá que inicien sesión o se registren.

Para comenzar, importemos las bibliotecas que instalamos previamente:

1
2
3
4
const express = require('express');
const exphbs = require('express-handlebars');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');

Usaremos el módulo ‘crypto’ nativo de Node para el hashing de contraseñas y para generar un token de autenticación; esto se explicará un poco más adelante en este artículo.

A continuación, creemos una aplicación Express simple y configuremos el middleware que hemos importado, junto con el motor Handlebars:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const app = express();

// To support URL-encoded bodies
app.use(bodyParser.urlencoded({ extended: true }));

// To parse cookies from the HTTP Request
app.use(cookieParser());

app.engine('hbs', exphbs({
    extname: '.hbs'
}));

app.set('view engine', 'hbs');

// Our requests hadlers will be implemented here...

app.listen(3000);

De forma predeterminada en Handlebars, la extensión de la plantilla debe ser .handlebars. Como puede ver en este código, hemos configurado nuestro motor de plantillas de manillares para admitir archivos con la extensión más corta .hbs. Ahora vamos a crear algunos archivos de plantilla:

template files

La carpeta layouts dentro de la carpeta view contendrá su diseño principal, que proporcionará el HTML base para otras plantillas.

Vamos a crear main.hbs, nuestra página contenedora principal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>

        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    </head>
    <body>

        <div class="container">
            {{{body}}}
        </div>

        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
    </body>
</html>

Otras plantillas aparecerán dentro de la etiqueta {{{body}}} de esta plantilla. Tenemos el modelo HTML y los archivos CSS y JS necesarios para Oreja importados en este diseño.

Con nuestro contenedor principal terminado, creemos la página home.hbs, donde se les pedirá a los usuarios que inicien sesión o se registren:

1
2
3
4
5
6
7
8
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">Simple Authentication App</a>
</nav>

<div style="margin-top: 30px">
    <a class="btn btn-primary btn-lg active" href="/login">Login</a>
    <a class="btn btn-primary btn-lg active" href="/register">Register</a>
</div>

Luego, creemos un controlador de solicitudes para la ruta raíz (/) para representar la plantilla de inicio.

1
2
3
app.get('/', function (req, res) {
    res.render('home');
});

Iniciemos nuestra aplicación y naveguemos a http://localhost:3000:

application homepage

Registro de cuenta {#registro de cuenta}

La información sobre una cuenta se recopila a través de una página registration.hbs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<div class="row justify-content-md-center" style="margin-top: 30px">
    <div class="col-md-4">

        {{#if message}}
            <div class="alert {{messageClass}}" role="alert">
                {{message}}
            </div>
        {{/if}}

        <form method="POST" action="/register">
            <div class="form-group">
                <label for="firstNameInput">First Name</label>
                <input name="firstName" type="text" class="form-control" id="firstNameInput">
            </div>

            <div class="form-group">
                <label for="lastNameInput">Last Name</label>
                <input name="firstName" type="text" class="form-control" id="lastNameInput">
            </div>

            <div class="form-group">
                <label for="emailInput">Email address</label>
                <input name="email" type="email" class="form-control" id="emailInput" placeholder="Enter email">
            </div>

            <div class="form-group">
                <label for="passwordInput">Password</label>
                <input name="password" type="password" class="form-control" id="passwordInput" placeholder="Password">
            </div>

            <div class="form-group">
                <label for="confirmPasswordInput">Confirm Password</label>
                <input name="confirmPassword" type="password" class="form-control" id="confirmPasswordInput"
                    placeholder="Re-enter your password here">
            </div>

            <button type="submit" class="btn btn-primary">Login</button>
        </form>
    </div>
</div>

En esta plantilla, hemos creado un formulario con campos de registro del usuario que es el Nombre, Apellido, Dirección de correo electrónico, Contraseña y Confirmar contraseña y configuramos nuestra acción como la ruta /registrarse. Además, tenemos un campo de mensaje en el que mostraremos mensajes de error y éxito para un ejemplo si las contraseñas no coinciden, etc.

Vamos a crear un identificador de solicitud para representar la plantilla de registro cuando el usuario visite http://localhost:3000/register:

1
2
3
app.get('/register', (req, res) => {
    res.render('register');
});

Debido a problemas de seguridad, es una buena práctica codificar la contraseña con un [algoritmo hash] fuerte (https://en.wikipedia.org/wiki/Secure_Hash_Algorithms) como SHA256. Al codificar las contraseñas, nos aseguramos de que, incluso si nuestra base de datos de contraseñas se ve comprometida, las contraseñas no están simplemente a la vista en formato de texto.

Un método aún mejor que el simple hash es usar sal, como con [crypta](https://en.wikipedia.org/ wiki/Bcrypt) algoritmo. Para obtener más información sobre cómo proteger la autenticación, consulte [Implementación correcta de la autenticación de usuario](/implementación-de-la-autenticación-de-usuario-de-la-manera correcta/). En este artículo, sin embargo, mantendremos las cosas un poco más simples.

1
2
3
4
5
6
7
const crypto = require('crypto');

const getHashedPassword = (password) => {
    const sha256 = crypto.createHash('sha256');
    const hash = sha256.update(password).digest('base64');
    return hash;
}

Cuando el usuario envía el formulario de registro, se enviará una solicitud ‘POST’ a la ruta ‘/registro’.

Dicho esto, ahora debemos manejar esa solicitud con la información del formulario y conservar nuestro usuario recién creado. Por lo general, esto se hace conservando al usuario en una base de datos, pero en aras de la simplicidad, almacenaremos a los usuarios en una matriz de JavaScript.

Dado que cada reinicio del servidor reiniciará la matriz, codificaremos un usuario con fines de prueba para que se inicialice cada vez:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const users = [
    // This user is added to the array to avoid creating a new user on each restart
    {
        firstName: 'John',
        lastName: 'Doe',
        email: '[correo electrónico protegido]',
        // This is the SHA256 hash for value of `password`
        password: 'XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg='
    }
];

app.post('/register', (req, res) => {
    const { email, firstName, lastName, password, confirmPassword } = req.body;

    // Check if the password and confirm password fields match
    if (password === confirmPassword) {

        // Check if user with the same email is also registered
        if (users.find(user => user.email === email)) {

            res.render('register', {
                message: 'User already registered.',
                messageClass: 'alert-danger'
            });

            return;
        }

        const hashedPassword = getHashedPassword(password);

        // Store user into the database if you are using one
        users.push({
            firstName,
            lastName,
            email,
            password: hashedPassword
        });

        res.render('login', {
            message: 'Registration Complete. Please login to continue.',
            messageClass: 'alert-success'
        });
    } else {
        res.render('register', {
            message: 'Password does not match.',
            messageClass: 'alert-danger'
        });
    }
});

El email recibido, firstName, lastName, password y confirmPassword se validan: las contraseñas coinciden, el correo electrónico aún no está registrado, etc.

Si cada validación tiene éxito, codificamos la contraseña y almacenamos la información dentro de la matriz y redirigimos al usuario a la página de inicio de sesión. De lo contrario, volveremos a mostrar la página de registro con el mensaje de error.

Ahora, visitemos el punto final /register para validar que esté funcionando correctamente:

application registration page

Acceso a la cuenta

Con el registro fuera del camino, podemos implementar la funcionalidad de inicio de sesión. Comencemos creando la página login.hbs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="row justify-content-md-center" style="margin-top: 100px">
    <div class="col-md-6">

        {{#if message}}
            <div class="alert {{messageClass}}" role="alert">
                {{message}}
            </div>
        {{/if}}

        <form method="POST" action="/login">
            <div class="form-group">
                <label for="exampleInputEmail1">Email address</label>
                <input name="email" type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
            </div>
            <div class="form-group">
                <label for="exampleInputPassword1">Password</label>
                <input name="password" type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
            </div>
            <button type="submit" class="btn btn-primary">Login</button>
        </form>
    </div>
</div>

Y luego, creemos también un controlador para esa solicitud:

1
2
3
app.get('/login', (req, res) => {
    res.render('login');
});

Este formulario enviará una solicitud POST a /login cuando el usuario envíe el formulario. Sin embargo, otra cosa que haremos será enviar un token de autenticación para el inicio de sesión. Este token se utilizará para identificar al usuario y cada vez que envíe una solicitud HTTP, este token se enviará como una cookie:

1
2
3
const generateAuthToken = () => {
    return crypto.randomBytes(30).toString('hex');
}

Con nuestro método auxiliar, podemos crear un controlador de solicitudes para la página de inicio de sesión:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// This will hold the users and authToken related to users
const authTokens = {};

app.post('/login', (req, res) => {
    const { email, password } = req.body;
    const hashedPassword = getHashedPassword(password);

    const user = users.find(u => {
        return u.email === email && hashedPassword === u.password
    });

    if (user) {
        const authToken = generateAuthToken();

        // Store authentication token
        authTokens[authToken] = user;

        // Setting the auth token in cookies
        res.cookie('AuthToken', authToken);

        // Redirect user to the protected page
        res.redirect('/protected');
    } else {
        res.render('login', {
            message: 'Invalid username or password',
            messageClass: 'alert-danger'
        });
    }
});

En este controlador de solicitudes, se usa un mapa llamado authTokens para almacenar tokens de autenticación como clave y el usuario correspondiente como valor, lo que permite una simple búsqueda de token a usuario. Puede usar una base de datos como redis, o realmente, cualquier base de datos para almacenar estos tokens; estamos usando este mapa para simplificar.

Al presionar el punto final /login, seremos recibidos con:

application login page

Sin embargo, aún no hemos terminado. Tendremos que inyectar al usuario la solicitud leyendo el authToken de las cookies al recibir la solicitud de inicio de sesión. Por encima de todos los controladores de solicitudes y debajo del middleware cookie-parser, creemos nuestro propio middleware personalizado para inyectar a los usuarios las solicitudes:

1
2
3
4
5
6
7
8
9
app.use((req, res, next) => {
    // Get auth token from the cookies
    const authToken = req.cookies['AuthToken'];

    // Inject the user to the request
    req.user = authTokens[authToken];

    next();
});

Ahora podemos usar req.user dentro de nuestros controladores de solicitudes para verificar si el usuario está autenticado a través de un token.

Finalmente, vamos a crear un controlador de solicitudes para representar la página protegida - protected.hbs:

1
2
3
4
5
6
7
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <a class="navbar-brand" href="#">Protected Page</a>
</nav>

<div>
    <h2>This page is only visible to logged in users</h2>
</div>

Y un controlador de solicitud para la página:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app.get('/protected', (req, res) => {
    if (req.user) {
        res.render('protected');
    } else {
        res.render('login', {
            message: 'Please login to continue',
            messageClass: 'alert-danger'
        });
    }
});

Como puede ver, puede usar req.user para verificar si el usuario está autenticado. Si ese objeto está vacío, el usuario no está autenticado.

Otra forma de solicitar la autenticación en las rutas es implementarla como middleware, que luego se puede aplicar a las rutas directamente tal como están definidas con el objeto app:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const requireAuth = (req, res, next) => {
    if (req.user) {
        next();
    } else {
        res.render('login', {
            message: 'Please login to continue',
            messageClass: 'alert-danger'
        });
    }
};

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

Las estrategias de autorización también se pueden implementar de esta manera asignando roles a los usuarios y luego verificando los permisos correctos antes de que el usuario acceda a la página.

Conclusión

La autenticación de usuario en Express es bastante simple y directa. Usamos el módulo ‘crypto’ nativo de Node para codificar las contraseñas de los usuarios registrados como una característica básica de seguridad, y creamos una página protegida, visible solo para los usuarios autenticados con un token.

El código fuente de este proyecto se puede encontrar en GitHub.