Manejo de la autenticaci贸n en 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 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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *