Guía de autenticación de Node.js sin passport.js y servicios de terceros

El autor del artículo, cuya traducción publicamos hoy, dice que ahora puede observar la creciente popularidad de los servicios de autenticación como Google Firebase Authentication, AWS Cognito y Auth0. Las soluciones genéricas como passport.js se han convertido en el estándar de la industria. Pero, dada la situación actual, se ha convertido en un lugar común que los desarrolladores nunca entiendan completamente qué mecanismos están involucrados en la operación de los sistemas de autenticación.

Este material está dedicado al problema de organizar la autenticación de usuarios en Node.js. En él, en un ejemplo práctico, se considera la organización del registro de usuarios en el sistema y la organización de su entrada en el sistema. Planteará problemas como trabajar con la tecnología JWT y la suplantación del usuario.



Además, preste atención a este repositorio de GitHub, que contiene el código para el proyecto Node.js, algunos ejemplos de los cuales se dan en este artículo. Puede usar este repositorio como base para sus propios experimentos.

Requisitos del proyecto


Aquí están los requisitos para el proyecto que trataremos aquí:

  • La presencia de una base de datos en la que se almacenarán la dirección de correo electrónico y la contraseña del usuario, ya sea clientId y clientSecret, o una especie de combinación de claves privadas y públicas.
  • Usando un algoritmo criptográfico fuerte y eficiente para encriptar una contraseña.

En el momento en que escribo este material, creo que el mejor de los algoritmos criptográficos existentes es Argon2. Le pido que no use algoritmos criptográficos simples como SHA256, SHA512 o MD5.

Además, le sugiero que eche un vistazo a este maravilloso material, en el que puede encontrar detalles sobre cómo elegir un algoritmo para contraseñas hash.

Registro de usuarios en el sistema.


Cuando se crea un nuevo usuario en el sistema, su contraseña debe ser cifrada y almacenada en la base de datos. La contraseña se almacena en la base de datos junto con la dirección de correo electrónico y otra información sobre el usuario (por ejemplo, entre ellos puede haber un perfil de usuario, tiempo de registro, etc.).

import * as argon2 from 'argon2'; class AuthService {   public async SignUp(email, password, name): Promise<any> {     const passwordHashed = await argon2.hash(password);     const userRecord = await UserModel.create({       password: passwordHashed,       email,       name,     });     return {       //    - !!!       user: {         email: userRecord.email,         name: userRecord.name,       },       } 

La información de la cuenta de usuario debería verse como la siguiente.


Datos de usuario recuperados de MongoDB usando Robo3T

Inicio de sesión de usuario


Aquí hay un diagrama de las acciones realizadas cuando un usuario intenta iniciar sesión.


Inicio de sesión de usuario

Esto es lo que sucede cuando un usuario inicia sesión:

  • El cliente envía al servidor una combinación del identificador público y la clave privada del usuario. Esta suele ser una dirección de correo electrónico y una contraseña.
  • El servidor busca al usuario en la base de datos por dirección de correo electrónico.
  • Si el usuario existe en la base de datos, el servidor codifica la contraseña que se le envió y compara lo que sucedió con el hash de contraseña almacenado en la base de datos.
  • Si la verificación es exitosa, el servidor genera un llamado token o token de autenticación: JSON Web Token (JWT).

JWT es una clave temporal. El cliente debe enviar esta clave al servidor con cada solicitud al punto final autenticado.

 import * as argon2 from 'argon2'; class AuthService {  public async Login(email, password): Promise<any> {    const userRecord = await UserModel.findOne({ email });    if (!userRecord) {      throw new Error('User not found')    } else {      const correctPassword = await argon2.verify(userRecord.password, password);      if (!correctPassword) {        throw new Error('Incorrect password')            return {      user: {        email: userRecord.email,        name: userRecord.name,      },      token: this.generateJWT(userRecord),    } 

La verificación de contraseña se realiza utilizando la biblioteca argon2. Esto es para prevenir los llamados " ataques de tiempo ". Al realizar dicho ataque, un atacante intenta descifrar la contraseña por la fuerza bruta, basándose en un análisis de cuánto tiempo necesita el servidor para formar una respuesta.

Ahora hablemos sobre cómo generar JWT.

¿Qué es un JWT?


JSON Web Token (JWT) es un objeto JSON codificado en forma de cadena. Los tokens se pueden tomar como un sustituto de las cookies, lo que tiene varias ventajas sobre ellas.

El token consta de tres partes. Este es el encabezado, la carga útil y la firma. La siguiente figura muestra su apariencia.


Jwt

Los datos de token se pueden decodificar en el lado del cliente sin el uso de una clave secreta o firma.

Esto puede ser útil para transferir, por ejemplo, metadatos codificados dentro del token. Dichos metadatos pueden describir la función del usuario, su perfil, la duración del token, etc. Se pueden utilizar en aplicaciones de front-end.

Así es como se vería una ficha decodificada.


Token Decodificado

Generando JWT en Node.js


Creemos la función generateToken que necesitamos para completar el trabajo en el servicio de autenticación de usuario.

Puede crear JWT utilizando la biblioteca jsonwebtoken. Puedes encontrar esta biblioteca en npm.

 import * as jwt from 'jsonwebtoken' class AuthService {  private generateToken(user) {    const data = {      _id: user._id,      name: user.name,      email: user.email    };    const signature = 'MySuP3R_z3kr3t';    const expiration = '6h';    return jwt.sign({ data, }, signature, { expiresIn: expiration }); } 

Lo más importante aquí son los datos codificados. No envíe información secreta del usuario en tokens.

Una firma (aquí es la constante de signature ) son los datos secretos que se utilizan para generar el JWT. Es muy importante asegurarse de que la firma no caiga en las manos equivocadas. Si la firma se ve comprometida, el atacante podrá generar tokens en nombre de los usuarios y robar sus sesiones.

Endpoint Protection y JWT Validation


Ahora el código del cliente debe enviar un JWT en cada solicitud a un punto final seguro.

Se recomienda que incluya JWT en los encabezados de solicitud. Generalmente se incluyen en el encabezado de Autorización.


Encabezado de autorización

Ahora, en el servidor, debe crear código que sea middleware para rutas express. Pon este código en el archivo isAuth.ts :

 import * as jwt from 'express-jwt'; //      ,  JWT      Authorization,        req.body,    ,      ,     . const getTokenFromHeader = (req) => {  if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {    return req.headers.authorization.split(' ')[1]; } export default jwt({  secret: 'MySuP3R_z3kr3t', //      ,     JWT  userProperty: 'token', //       ,     services/auth:generateToken -> 'req.token'  getToken: getTokenFromHeader, //        }) 

Es útil poder obtener información completa sobre la cuenta de usuario de la base de datos y adjuntarla a la solicitud. En nuestro caso, esta característica se implementa utilizando middleware del archivo attachCurrentUser.ts . Aquí está su código simplificado:

 export default (req, res, next) => { const decodedTokenData = req.tokenData; const userRecord = await UserModel.findOne({ _id: decodedTokenData._id })  req.currentUser = userRecord; if(!userRecord) {   return res.status(401).end('User not found') } else {   return next(); } 

Después de implementar este mecanismo, las rutas podrán recibir información sobre el usuario que está ejecutando la solicitud:

 import isAuth from '../middlewares/isAuth';  import attachCurrentUser from '../middlewares/attachCurrentUser';  import ItemsModel from '../models/items';  export default (app) => {    app.get('/inventory/personal-items', isAuth, attachCurrentUser, (req, res) => {      const user = req.currentUser;      const userItems = await ItemsModel.find({ owner: user._id });      return res.json(userItems).status(200);    }) 

La ruta de inventory/personal-items ahora está protegida. Para acceder a él, el usuario debe tener un JWT válido. Además, una ruta puede utilizar la información del usuario para buscar en la base de datos la información que necesita.

¿Por qué se protegen los tokens de los intrusos?


Después de leer sobre el uso de JWT, puede hacerse la siguiente pregunta: "Si los datos de JWT pueden decodificarse en el lado del cliente, ¿es posible procesar el token de tal manera que cambie la identificación del usuario u otros datos?".

Decodificación de tokens: la operación es muy simple. Sin embargo, no puede "rehacer" este token sin tener esa firma, esos datos secretos que se usaron al firmar el JWT en el servidor.

Es por eso que la protección de estos datos sensibles es tan importante.

Nuestro servidor verifica la firma en el middleware isAuth. La biblioteca express-jwt es responsable de verificar.

Ahora, después de descubrir cómo funciona la tecnología JWT, hablemos de algunas características adicionales interesantes que nos brinda.

¿Cómo hacerse pasar por un usuario?


La suplantación de usuario es una técnica utilizada para iniciar sesión en un sistema como un usuario específico sin conocer su contraseña.

Esta característica es muy útil para superadministradores, desarrolladores o personal de soporte. La suplantación les permite resolver problemas que aparecen solo en el curso de los usuarios que trabajan con el sistema.

Puede trabajar con la aplicación en nombre del usuario sin conocer su contraseña. Para hacer esto, es suficiente generar un JWT con la firma correcta y con los metadatos necesarios que describen al usuario.

Cree un punto final que pueda generar tokens para ingresar al sistema bajo la apariencia de usuarios específicos. Solo el superadministrador del sistema puede usar este punto final.

Para empezar, debemos asignar a este usuario un rol con un nivel de privilegio más alto que otros usuarios. Esto se puede hacer de muchas maneras diferentes. Por ejemplo, simplemente agregando el campo de role a la información del usuario almacenada en la base de datos.

Puede parecerse al que se muestra a continuación.


Nuevo campo en la información del usuario

El valor del campo de role superadministrador es super-admin .

A continuación, debe crear un nuevo middleware que verifique el rol del usuario:

 export default (requiredRole) => {  return (req, res, next) => {    if(req.currentUser.role === requiredRole) {      return next();    } else {      return res.status(401).send('Action not allowed');    } 

Debe colocarse después de isAuth y attachCurrentUser. Ahora cree el punto final que genera el JWT para el usuario en nombre del cual el súper administrador desea iniciar sesión:

   import isAuth from '../middlewares/isAuth';  import attachCurrentUser from '../middlewares/attachCurrentUser';  import roleRequired from '../middlwares/roleRequired';  import UserModel from '../models/user';  export default (app) => {    app.post('/auth/signin-as-user', isAuth, attachCurrentUser, roleRequired('super-admin'), (req, res) => {      const userEmail = req.body.email;      const userRecord = await UserModel.findOne({ email: userEmail });      if(!userRecord) {        return res.status(404).send('User not found');           return res.json({        user: {          email: userRecord.email,          name: userRecord.name        },        jwt: this.generateToken(userRecord)      })      .status(200);    }) 

Como puede ver, no hay nada misterioso. El superadministrador conoce la dirección de correo electrónico del usuario en nombre de quien desea iniciar sesión. La lógica del código anterior recuerda mucho cómo funciona el código, proporcionando una entrada al sistema de los usuarios comunes. La principal diferencia es que la contraseña no está marcada aquí.
La contraseña no se verifica aquí debido al hecho de que simplemente no se necesita aquí. La seguridad del punto final es proporcionada por middleware.

Resumen


No hay nada de malo en confiar en servicios y bibliotecas de autenticación de terceros. Esto ayuda a los desarrolladores a ahorrar tiempo. Pero también deben conocer los principios en los que se basa el funcionamiento de los sistemas de autenticación y qué garantiza el funcionamiento de dichos sistemas.

En este artículo, exploramos las posibilidades de autenticación JWT, hablamos sobre la importancia de elegir un buen algoritmo criptográfico para contraseñas hash. Examinamos la creación de un mecanismo de suplantación de usuario.

Hacer lo mismo con algo como passport.js está lejos de ser fácil. La autenticación es un gran tema. Quizás volvamos a ella.

Estimados lectores! ¿Cómo se crean sistemas de autenticación para sus proyectos Node.js?

Source: https://habr.com/ru/post/457700/


All Articles