Manejo de la autenticación en Express.js

M

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 deba 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 o 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 esqueleto de Node.js:

$ npm init

Ahora, también podemos instalar Express:

$ npm install --save express

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

Sigamos adelante e instalemos express-handlebars:

$ npm install --save express-handlebars

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

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

Implementación

La aplicación que vamos a crear 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 hemos instalado previamente:

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

Usaremos el nativo del node crypto módulo para el hash de contraseña y para generar un token de autenticación; esto se explicará un poco más adelante en el artículo.

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

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);

Por defecto en Manillares, 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 el .hbs extensión más corta. Ahora creemos algunos archivos de plantilla:

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

Vamos a crear el main.hbs, nuestra página principal de envoltura:

<!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 se renderizarán dentro del {{{body}}} etiqueta de esta plantilla. Tenemos la plantilla HTML y los archivos CSS y JS necesarios para Oreja importado en este diseño.

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

<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.

app.get("https://Pharos.sh.com/", function (req, res) {
    res.render('home');
});

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

Registro de cuenta

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

<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 establecemos nuestra acción como /register ruta. 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.

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

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 me gusta SHA256. Al aplicar hash a las contraseñas, nos aseguramos de que, incluso si nuestra base de datos de contraseñas pudiera verse comprometida, las contraseñas no están simplemente ahí a la vista en formato de texto.

Un método aún mejor que el simple hash es usar sal, como con el 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. En este artículo, sin embargo, simplificaremos un poco las cosas.

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, un POST la solicitud será enviada al /register camino.

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

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

const users = [
    // This user is added to the array to avoid creating a new user on each restart
    {
        firstName: 'John',
        lastName: 'Doe',
        email: '[email protected]',
        // 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 recibido email, firstName, lastName, passwordy confirmPassword están validadas: las contraseñas coinciden, el correo electrónico aún no está registrado, etc.

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

Ahora, visitemos el /register endpoint para validar que está funcionando correctamente:

Cuenta de Ingreso

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

<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 un controlador para esa solicitud también:

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

Este formulario enviará un POST solicitud al /login cuando el usuario envía el formulario. Sin embargo, otra cosa que haremos es 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:

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:

// 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, un mapa llamado authTokens se utiliza para almacenar tokens de autenticación como clave y el usuario correspondiente como valor, lo que permite un token simple para la búsqueda del usuario. Puedes usar una base de datos como Redis, o en realidad, cualquier base de datos para almacenar estos tokens; estamos usando este mapa para simplificar.

Golpeando el /login endpoint, seremos recibidos con:

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

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 mediante un token.

Finalmente, creemos un controlador de solicitudes para representar la página protegida: protected.hbs:

<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 solicitudes para la página:

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 comprobar si el usuario está autenticado. Si ese objeto está vacío, el usuario no está autenticado.

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

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. Hemos utilizado el nativo de Node crypto módulo para codificar contraseñas de usuarios registrados como una característica de seguridad básica, y creó una página protegida, visible solo para usuarios autenticados con un token.

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

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad