Revista Informática

Registro de usuarios (8ª parte de creación de una API REST con Express y TypeScript)

Publicado el 30 noviembre 2022 por Daniel Rodríguez @analyticslane
Registro de usuarios (8ª parte de creación de una API REST con Express y TypeScript)

En la entrega anterior se vio una forma para proteger las rutas mediante el uso de JWT (JSON Web Token). Aunque, dado que el API aún no cuenta con usuarios, cualquier que conozca la ruta puede obtener un token válido y acceder a la misma. En esta entrega vamos a ver cómo crear un proceso de registros de usuarios para nuestra API.

Función de hashing para contraseñas

Para guardar las contraseñas en la base de datos es necesario contar con una función de hashing. No es una buena práctica guardar las contraseñas encriptadas, cualquier que acceda a la aplicación tendrá la posibilidad de conseguir la clave de encriptado y recuperar de este modo las contraseñas. Por otro lado, almacenarlas sin proteger es una irresponsabilidad hacia los usuarios.

El principal motivo por el que se debe almacenar el hash de la contraseña en lugar de está es porque las funciones de hashing son no reversibles. Esto es, a partir de un hash no se puede obtener el texto original. Debido a esta propiedad, aunque un atacante pueda obtener la tabla con los hashes de los usuarios, no podrá recuperar las contraseñas de una forma eficiente. Solamente mediante un ataque de fuerza bruta, generando hashes con todas las combinaciones de caracteres hasta que encuentre una que pueda generar un hash de la lista.

Uno de los algoritmos más utilizados para crear hashes de contraseñas es bcrypt. Debido a que la generación de un hash no es especialmente rápido, un ataque por fuerza bruta requerirá de mucha potencia de cálculo. En Node existe el paquete bcrypt en el que se implementa este algoritmo, para instalar solamente habrá que escribir en la terminal el comando

npm install bcrypt

Para trabajar en TypeScript también se deberán instalar las definiciones de tipos

npm install @types/bcrypt --save-dev

Creación de una tabla en la base de datos

En primer lugar, es necesario crear una tabla para almacenar los usuarios y las contraseñas. Para ello se irá a la carpeta entities y se creará un nuevo archivo llamado logins.ts en el que se escribirá el siguiente código.

import { compareSync, genSaltSync, hashSync } from 'bcrypt';
import { BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export default class Logins {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;

  @BeforeInsert()
  hashPassword() {
    this.password = hashSync(this.password, genSaltSync(10));
  }

  validatePassword(password: string): boolean {
    return compareSync(password, this.password);
  }
}

Como se puede ver se ha creado una envida de TypeORM en el que se tiene un id, una cadena de texto para almacenar el nombre de usuarios y otra cadena de texto para almacenar la contraseña.

En esta ocasión se han creado dos métodos en la clase hashPassword() y validatePassword(). El primero método se ejecuta inmediatamente antes de guardar el objeto en la base de datos, lo que se configura con el embellecedor BeforeInsert, y lo que hace es reemplazar la contraseña por el hash. Para lo que recurre a la función hashSync del paquete bcrypt. A la que se le debe pasar el password y una salt generada con la función genSaltSync(). El segundo método, validatePassword(), comprueba que una contraseña coincide con el hash almacenado en la base de datos. Para esto recurre a la función compareSync() que recibe como entrada una contraseña y un hash, en el caso de coincidir devuelve verdadero y falsos en cualquier otro.

La tabla que se ha creado es básica, en el caso de necesidad se podría agregar otros campos para almacenar grupos o roles de los usuarios.

Registro de usuarios

Ahora es necesario contar con la capacidad de registrar usuarios, para lo que en el archivo routes/auth.ts se agrega una nueva ruta register en la que se agrega el siguiente código.

router.post('/register', (req: Request, res: Response) => {
  const username = String(req.body.username);
  const password = String(req.body.password);

  datasource
    .getRepository(Logins)
    .findOneByOrFail({ username })
    .then(() => {
      responseAndLogger(res, 'User already exists', 406);
    })
    .catch(() => {
      const user = new Logins();
      user.username = username;
      user.password = password;

      datasource
        .getRepository(Logins)
        .save(user)
        .then((user) => res.send(user))
        .catch((error) => responseAndLogger(res, error.message, 500));
    });
});

El código obtiene el usuario y la contraseña del cuerpo. En primer lugar, busca si el usuario existe, en caso de que sea así devele un error indicado que el usuario existe y no se puede crear una nueva. Por otro lado, si el nombre de usuario está libre, creará un nuevo objeto, lo almacena en la base de datos y devuelve este. En ese caso no es un problema devolver el usuario ya que la contraseña se encuentra protegida y no se puede obtener a partir del hash. Aunque para mayor seguridad se puede devolver solo el id o una confirmación de la creación.

Ahora accediendo a la ruta http://localhost:4000/auth/register se puede registrar un usuario mediante una llamada POST. Para ello se debe incluir un JSON con el usuario y el nombre en el cuerpo.

Nótese que actualmente cualquier persona que conozca la ruta podrá registrar usuarios. En una API en será necesario proteger esta ruta mediante un middleware para evitar que se puedan crear usuarios.

Obtención de un token

Una vez se tiene la capacidad de crear usuarios, en el mismo archivo routes/auth.ts, se debe modificar la ruta login con el siguiente código.

router.post('/login', (req: Request, res: Response) => {
  const username = String(req.body.username);
  const password = String(req.body.password);

  datasource
    .getRepository(Logins)
    .findOneByOrFail({ username })
    .then((user) => {
      if (user.validatePassword(password)) {
        sign({ id: user.id, username: user.username }, secret, { expiresIn }, (err, token) => {
          if (err) {
            responseAndLogger(res, 'It was not possible to generate the token', 400);
          }

          return res.send({ token });
        });
      } else {
        responseAndLogger(res, 'Invalid password', 400);
      }
    })
    .catch(() => responseAndLogger(res, 'Invalid user', 400));
});

En este caso también se recupera el usuario y la contraseña del cuerpo. A diferencia del caso anterior busca el usuario en la base de datos y, en el caso de localizar uno con el mismo nombre, comprobará si la contraseña es válida. En el caso afirmativo devolverá un token con el id de usuario y su nombre y un fallo en cualquier otra situación. Un ejemplo de respuesta válida se puede ver en la siguiente captura de pantalla.

Por otro lado, cuando el usuario no es válido la respuesta seria como la siguiente.

Nótese que en este caso se devele un error diferente si el usuario no existe o la contraseña no es válida. A nivel de seguridad lo mejor sería devolver siempre el mismo mensaje para no dar pistas a un posible atacante. Aunque en este caso es útil para saber la ruta que ha seguido el código.

Conclusiones

En esta entrega se ha visto como una opción para el registro de usuarios en el API. Existen algunas librerías que facilitan esta tarea, como puede ser el caso de Passport, pero esta es una solución sencilla que puede ser utilizada en la mayoría de los casos. La próxima entrega se verá cómo incluir un certificado para servir el API mediante HTTPS.

Imagen de Tayeb MEZAHDIA en Pixabay


Volver a la Portada de Logo Paperblog